From 192143bfb6c7d60e8a4da264ab5321dd3b5c73f7 Mon Sep 17 00:00:00 2001
From: Brunobrno
Date: Wed, 10 Jun 2026 23:59:40 +0200
Subject: [PATCH] wokring final build version
---
.claude/settings.local.json | 3 +-
.../notifications/migrations/0001_initial.py | 40 +
backend/requirements.txt | 1 +
...essagereaction_unique_together_and_more.py | 23 +
backend/vontor_cz/settings.py | 6 +-
backend/vontor_cz/storage.py | 82 +++
docker-compose.yml | 11 +-
.../models/apiNotificationsListParams.ts | 20 +
.../apiNotificationsReadAllCreate200.ts | 9 +
.../apiNotificationsUnreadCountRetrieve200.ts | 9 +
.../src/api/generated/private/models/index.ts | 6 +
.../generated/private/models/notification.ts | 36 +
.../private/models/notificationTypeEnum.ts | 25 +
.../models/paginatedNotificationList.ts | 15 +
.../private/notifications/notifications.ts | 684 ++++++++++++++++++
.../src/api/generated/public/models/index.ts | 3 +
.../generated/public/models/notification.ts | 36 +
.../public/models/notificationTypeEnum.ts | 25 +
.../models/paginatedNotificationList.ts | 15 +
.../src/components/downloader/Downloader.tsx | 117 ++-
.../home/ContactMe/ContactMeForm.tsx | 16 +-
.../src/components/home/Footer/footer.tsx | 25 +-
.../src/components/home/ads/Drone/Drone.tsx | 18 +-
.../components/home/drone/DroneSection.tsx | 44 +-
.../src/components/home/hero/HeroSection.tsx | 41 +-
.../src/components/home/navbar/SiteNav.tsx | 64 +-
.../components/home/navbar/navbar.module.css | 37 +-
.../components/home/projects/DemoModal.tsx | 19 +-
.../home/projects/ProjectsSection.tsx | 63 +-
.../src/components/home/services/Services.tsx | 123 ++--
.../src/components/home/tech/TechMarquee.tsx | 7 +-
.../components/home/webdev/WebDevSection.tsx | 95 +--
.../src/components/social/chat/Message.tsx | 2 +-
.../social/chat/MessageComposer.tsx | 4 +-
.../src/components/social/hub/HubCard.tsx | 7 +-
.../src/components/social/hub/HubHeader.tsx | 12 +-
frontend/src/components/social/posts/Post.tsx | 21 +-
.../components/social/posts/PostComposer.tsx | 4 +-
frontend/src/hooks/usePermissions.ts | 2 +-
frontend/src/i18n/index.ts | 10 +-
frontend/src/i18n/locales/cs/common.json | 19 +-
frontend/src/i18n/locales/cs/contact.json | 10 +
frontend/src/i18n/locales/cs/downloader.json | 46 ++
frontend/src/i18n/locales/cs/home.json | 146 ++++
frontend/src/i18n/locales/cs/services.json | 103 +++
frontend/src/i18n/locales/cs/social.json | 125 +++-
frontend/src/pages/contact/ContactPage.tsx | 27 +-
.../home/components/Services/droneServis.tsx | 83 ++-
.../components/Services/kinematografie.tsx | 91 +--
.../pages/home/components/Services/webs.tsx | 85 ++-
frontend/src/pages/home/home.tsx | 19 +-
.../src/pages/social/AccountSettingsPage.tsx | 56 +-
frontend/src/pages/social/HubPage.tsx | 34 +-
frontend/src/pages/social/HubsPage.tsx | 14 +-
frontend/src/pages/social/PostPage.tsx | 4 +-
frontend/src/pages/social/UserProfilePage.tsx | 2 +-
.../pages/social/account/AccountSettings.tsx | 41 +-
.../social/account/PasswordResetPage.tsx | 9 +-
.../src/pages/social/chat/ChatRoomPage.tsx | 10 +-
frontend/src/pages/social/hub/Create.tsx | 28 +-
frontend/src/pages/social/hub/Settings.tsx | 91 ++-
61 files changed, 2184 insertions(+), 639 deletions(-)
create mode 100644 backend/notifications/migrations/0001_initial.py
create mode 100644 backend/social/chat/migrations/0003_alter_messagereaction_unique_together_and_more.py
create mode 100644 backend/vontor_cz/storage.py
create mode 100644 frontend/src/api/generated/private/models/apiNotificationsListParams.ts
create mode 100644 frontend/src/api/generated/private/models/apiNotificationsReadAllCreate200.ts
create mode 100644 frontend/src/api/generated/private/models/apiNotificationsUnreadCountRetrieve200.ts
create mode 100644 frontend/src/api/generated/private/models/notification.ts
create mode 100644 frontend/src/api/generated/private/models/notificationTypeEnum.ts
create mode 100644 frontend/src/api/generated/private/models/paginatedNotificationList.ts
create mode 100644 frontend/src/api/generated/private/notifications/notifications.ts
create mode 100644 frontend/src/api/generated/public/models/notification.ts
create mode 100644 frontend/src/api/generated/public/models/notificationTypeEnum.ts
create mode 100644 frontend/src/api/generated/public/models/paginatedNotificationList.ts
create mode 100644 frontend/src/i18n/locales/cs/contact.json
create mode 100644 frontend/src/i18n/locales/cs/downloader.json
create mode 100644 frontend/src/i18n/locales/cs/home.json
create mode 100644 frontend/src/i18n/locales/cs/services.json
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 643568f..43f1d5b 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -19,7 +19,8 @@
"Bash(Get-ChildItem -Path \"c:\\\\Users\\\\bruno\\\\Documents\\\\GitHub\\\\vontor-cz\\\\backend\\\\\" -Directory | Select-Object -ExpandProperty Name)",
"PowerShell(Get-Command python)",
"PowerShell(python -c \"import django; print\\(django.__version__\\)\")",
- "WebSearch"
+ "WebSearch",
+ "Bash(npm run *)"
]
}
}
diff --git a/backend/notifications/migrations/0001_initial.py b/backend/notifications/migrations/0001_initial.py
new file mode 100644
index 0000000..19307bc
--- /dev/null
+++ b/backend/notifications/migrations/0001_initial.py
@@ -0,0 +1,40 @@
+# Generated by Django 5.2.7 on 2026-06-10 17:13
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_deleted', models.BooleanField(default=False)),
+ ('deleted_at', models.DateTimeField(blank=True, null=True)),
+ ('title', models.CharField(help_text='Předmět oznámení.', max_length=200)),
+ ('text', models.TextField(help_text='Obsah oznámení.')),
+ ('notification_type', models.CharField(choices=[('system', 'Systém'), ('order', 'Objednávka'), ('payment', 'Platba'), ('social', 'Sociální'), ('chat', 'Chat'), ('advertisement', 'Inzerát')], default='system', help_text='Kategorie oznámení — používá se pro ikonky a filtrování na frontendu.', max_length=20)),
+ ('action_url', models.CharField(blank=True, help_text="Volitelný odkaz na detail (např. '/objednavky/123/').", max_length=500, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('bulk', models.BooleanField(default=False, help_text='True, pokud bylo oznámení vytvořeno hromadně pro více uživatelů.')),
+ ('is_read', models.BooleanField(default=False)),
+ ('read_at', models.DateTimeField(blank=True, null=True)),
+ ('send_email', models.BooleanField(default=False, help_text='True, pokud byl zároveň odeslán e-mail.')),
+ ('email_subject', models.CharField(blank=True, max_length=255, null=True)),
+ ('email_template_path', models.CharField(blank=True, max_length=255, null=True)),
+ ('user', models.ForeignKey(help_text='Příjemce oznámení.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ['-created_at'],
+ },
+ ),
+ ]
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 0e53980..f990c14 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -24,6 +24,7 @@ django-constance #allows you to store and manage settings of page in the Django
# -- OBJECT STORAGE --
Pillow #adds image processing capabilities to your Python interpreter
+pillow-heif #HEIC/HEIF support for Pillow (iPhone photos)
whitenoise #pomáha se spuštěním serveru a načítaní static files
diff --git a/backend/social/chat/migrations/0003_alter_messagereaction_unique_together_and_more.py b/backend/social/chat/migrations/0003_alter_messagereaction_unique_together_and_more.py
new file mode 100644
index 0000000..2dfe4a9
--- /dev/null
+++ b/backend/social/chat/migrations/0003_alter_messagereaction_unique_together_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.7 on 2026-06-10 17:13
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('chat', '0002_chatreadstatus'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='messagereaction',
+ unique_together=set(),
+ ),
+ migrations.AddConstraint(
+ model_name='messagereaction',
+ constraint=models.UniqueConstraint(condition=models.Q(('is_deleted', False)), fields=('message', 'user'), name='unique_active_reaction_per_user_message'),
+ ),
+ ]
diff --git a/backend/vontor_cz/settings.py b/backend/vontor_cz/settings.py
index 27ad317..368cc74 100644
--- a/backend/vontor_cz/settings.py
+++ b/backend/vontor_cz/settings.py
@@ -634,7 +634,7 @@ STATIC_ROOT = BASE_DIR / 'collectedstaticfiles'
if not USE_S3:
# Local filesystem — development only
STORAGES = {
- "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
+ "default": {"BACKEND": "vontor_cz.storage.LocalMediaStorage"},
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
}
MEDIA_URL = os.getenv("MEDIA_URL", "/media/")
@@ -650,8 +650,8 @@ else:
S3_PROTO = 'https' if S3_SSL else 'http'
STORAGES = {
- "default": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"},
- "staticfiles": {"BACKEND": "storages.backends.s3boto3.S3StaticStorage"},
+ "default": {"BACKEND": "vontor_cz.storage.MediaStorage"},
+ "staticfiles": {"BACKEND": "vontor_cz.storage.StaticStorage"},
}
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
diff --git a/backend/vontor_cz/storage.py b/backend/vontor_cz/storage.py
new file mode 100644
index 0000000..17bf0d0
--- /dev/null
+++ b/backend/vontor_cz/storage.py
@@ -0,0 +1,82 @@
+import io
+import os
+
+from django.core.files.base import ContentFile
+from django.core.files.storage import FileSystemStorage
+import pillow_heif
+from PIL import Image, ImageSequence
+
+pillow_heif.register_heif_opener()
+from storages.backends.s3boto3 import S3Boto3Storage, S3StaticStorage
+
+# All formats Pillow handles natively (no extra plugins needed).
+# Add '.heic' / '.heif' if you install pillow-heif.
+_IMAGE_EXTS = {
+ '.jpg', '.jpeg', '.jpe', '.jfif', # JPEG variants
+ '.png', # PNG
+ '.gif', # GIF (animated preserved)
+ '.bmp', '.dib', # BMP
+ '.tiff', '.tif', # TIFF
+ '.tga', # Truevision TGA
+ '.ico', # ICO (largest frame used)
+ '.ppm', '.pgm', '.pbm', '.pnm', # Portable pixmap family
+ '.pcx', # PCX
+ '.heic', '.heif', # Apple HEIC/HEIF (pillow-heif)
+}
+
+def _to_webp(content, quality: int = 85) -> io.BytesIO:
+ img = Image.open(content)
+ frames = list(ImageSequence.Iterator(img))
+ out = io.BytesIO()
+ if len(frames) > 1:
+ converted = [f.copy().convert('RGBA') for f in frames]
+ converted[0].save(
+ out,
+ format='WEBP',
+ save_all=True,
+ append_images=converted[1:],
+ loop=img.info.get('loop', 0),
+ quality=quality,
+ )
+ else:
+ img.convert('RGBA').save(out, format='WEBP', quality=quality)
+ out.seek(0)
+ return out
+
+
+class WebPConversionMixin:
+ """
+ Intercepts image/GIF uploads and converts them to WebP before storage.
+ Videos are saved as-is; use `vontor_cz.tasks.convert_video_to_webm` async
+ to transcode them to WebM after save.
+ """
+ WEBP_QUALITY: int = 85
+
+ def _save(self, name: str, content) -> str:
+ root, ext = os.path.splitext(name)
+ if ext.lower() in _IMAGE_EXTS:
+ try:
+ webp_data = _to_webp(content, self.WEBP_QUALITY)
+ content = ContentFile(webp_data.read())
+ name = root + '.webp'
+ except Exception:
+ content.seek(0)
+ return super()._save(name, content)
+
+
+class MediaStorage(WebPConversionMixin, S3Boto3Storage):
+ def exists(self, name):
+ if not name:
+ return False
+ return super().exists(name)
+
+
+class LocalMediaStorage(WebPConversionMixin, FileSystemStorage):
+ pass
+
+
+class StaticStorage(S3StaticStorage):
+ def exists(self, name):
+ if not name:
+ return False
+ return super().exists(name)
diff --git a/docker-compose.yml b/docker-compose.yml
index a04d2c1..ec6edeb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,14 +12,13 @@ services:
depends_on:
- db
- redis
- - rustfs
volumes:
- ./backend:/app
- static-data:/app/collectedstaticfiles
- media-data:/app/media
command: sh -c "
python manage.py migrate --noinput &&
- python manage.py collectstatic --clear --noinput &&
+ python manage.py collectstatic --noinput &&
python manage.py seed_app_config &&
gunicorn -k uvicorn.workers.UvicornWorker vontor_cz.asgi:application --bind 0.0.0.0:8000 --timeout 600 --graceful-timeout 30 --workers 2"
ports:
@@ -67,7 +66,6 @@ services:
- redis
- db
- backend
- - rustfs
networks:
- app_network
@@ -86,7 +84,6 @@ services:
- redis
- db
- backend
- - rustfs
networks:
- app_network
@@ -159,10 +156,4 @@ volumes:
type: none
o: bind
device: ./volumes/media
- rustfs-data:
- driver: local
- driver_opts:
- type: none
- o: bind
- device: ./volumes/rustfs
diff --git a/frontend/src/api/generated/private/models/apiNotificationsListParams.ts b/frontend/src/api/generated/private/models/apiNotificationsListParams.ts
new file mode 100644
index 0000000..3da741a
--- /dev/null
+++ b/frontend/src/api/generated/private/models/apiNotificationsListParams.ts
@@ -0,0 +1,20 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+
+export type ApiNotificationsListParams = {
+ /**
+ * Which field to use when ordering the results.
+ */
+ ordering?: string;
+ /**
+ * A page number within the paginated result set.
+ */
+ page?: number;
+ /**
+ * A search term.
+ */
+ search?: string;
+};
diff --git a/frontend/src/api/generated/private/models/apiNotificationsReadAllCreate200.ts b/frontend/src/api/generated/private/models/apiNotificationsReadAllCreate200.ts
new file mode 100644
index 0000000..67687fb
--- /dev/null
+++ b/frontend/src/api/generated/private/models/apiNotificationsReadAllCreate200.ts
@@ -0,0 +1,9 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+
+export type ApiNotificationsReadAllCreate200 = {
+ marked?: number;
+};
diff --git a/frontend/src/api/generated/private/models/apiNotificationsUnreadCountRetrieve200.ts b/frontend/src/api/generated/private/models/apiNotificationsUnreadCountRetrieve200.ts
new file mode 100644
index 0000000..b1e4e04
--- /dev/null
+++ b/frontend/src/api/generated/private/models/apiNotificationsUnreadCountRetrieve200.ts
@@ -0,0 +1,9 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+
+export type ApiNotificationsUnreadCountRetrieve200 = {
+ unread_count?: number;
+};
diff --git a/frontend/src/api/generated/private/models/index.ts b/frontend/src/api/generated/private/models/index.ts
index 027d857..2d1c512 100644
--- a/frontend/src/api/generated/private/models/index.ts
+++ b/frontend/src/api/generated/private/models/index.ts
@@ -14,6 +14,9 @@ export * from "./apiConfigurationShopConfigurationListParams";
export * from "./apiConfigurationVatRatesListParams";
export * from "./apiDeutschepostBulkOrdersListParams";
export * from "./apiDeutschepostOrdersListParams";
+export * from "./apiNotificationsListParams";
+export * from "./apiNotificationsReadAllCreate200";
+export * from "./apiNotificationsUnreadCountRetrieve200";
export * from "./apiSchemaRetrieve200Four";
export * from "./apiSchemaRetrieve200One";
export * from "./apiSchemaRetrieve200Three";
@@ -68,6 +71,8 @@ export * from "./messageFile";
export * from "./messageReaction";
export * from "./messageSend";
export * from "./messageSender";
+export * from "./notification";
+export * from "./notificationTypeEnum";
export * from "./orderCarrier";
export * from "./orderCreate";
export * from "./orderItemCreate";
@@ -84,6 +89,7 @@ export * from "./paginatedHubList";
export * from "./paginatedHubPermissionList";
export * from "./paginatedChatList";
export * from "./paginatedMessageList";
+export * from "./paginatedNotificationList";
export * from "./paginatedOrderReadList";
export * from "./paginatedPostList";
export * from "./paginatedProductImageList";
diff --git a/frontend/src/api/generated/private/models/notification.ts b/frontend/src/api/generated/private/models/notification.ts
new file mode 100644
index 0000000..1b97971
--- /dev/null
+++ b/frontend/src/api/generated/private/models/notification.ts
@@ -0,0 +1,36 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+import type { NotificationTypeEnum } from "./notificationTypeEnum";
+
+export interface Notification {
+ readonly id: number;
+ /** Předmět oznámení. */
+ readonly title: string;
+ /** Obsah oznámení. */
+ readonly text: string;
+ /** Kategorie oznámení — používá se pro ikonky a filtrování na frontendu.
+
+ * `system` - Systém
+ * `order` - Objednávka
+ * `payment` - Platba
+ * `social` - Sociální
+ * `chat` - Chat
+ * `advertisement` - Inzerát */
+ readonly notification_type: NotificationTypeEnum;
+ /**
+ * Volitelný odkaz na detail (např. '/objednavky/123/').
+ * @nullable
+ */
+ readonly action_url: string | null;
+ readonly is_read: boolean;
+ /** @nullable */
+ readonly read_at: Date | null;
+ readonly created_at: Date;
+ /** True, pokud bylo oznámení vytvořeno hromadně pro více uživatelů. */
+ readonly bulk: boolean;
+ /** True, pokud byl zároveň odeslán e-mail. */
+ readonly send_email: boolean;
+}
diff --git a/frontend/src/api/generated/private/models/notificationTypeEnum.ts b/frontend/src/api/generated/private/models/notificationTypeEnum.ts
new file mode 100644
index 0000000..ea760eb
--- /dev/null
+++ b/frontend/src/api/generated/private/models/notificationTypeEnum.ts
@@ -0,0 +1,25 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+
+/**
+ * * `system` - Systém
+ * `order` - Objednávka
+ * `payment` - Platba
+ * `social` - Sociální
+ * `chat` - Chat
+ * `advertisement` - Inzerát
+ */
+export type NotificationTypeEnum =
+ (typeof NotificationTypeEnum)[keyof typeof NotificationTypeEnum];
+
+export const NotificationTypeEnum = {
+ system: "system",
+ order: "order",
+ payment: "payment",
+ social: "social",
+ chat: "chat",
+ advertisement: "advertisement",
+} as const;
diff --git a/frontend/src/api/generated/private/models/paginatedNotificationList.ts b/frontend/src/api/generated/private/models/paginatedNotificationList.ts
new file mode 100644
index 0000000..2ce3168
--- /dev/null
+++ b/frontend/src/api/generated/private/models/paginatedNotificationList.ts
@@ -0,0 +1,15 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+import type { Notification } from "./notification";
+
+export interface PaginatedNotificationList {
+ count: number;
+ /** @nullable */
+ next?: string | null;
+ /** @nullable */
+ previous?: string | null;
+ results: Notification[];
+}
diff --git a/frontend/src/api/generated/private/notifications/notifications.ts b/frontend/src/api/generated/private/notifications/notifications.ts
new file mode 100644
index 0000000..3b85c4b
--- /dev/null
+++ b/frontend/src/api/generated/private/notifications/notifications.ts
@@ -0,0 +1,684 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+import { useMutation, useQuery } from "@tanstack/react-query";
+import type {
+ DataTag,
+ DefinedInitialDataOptions,
+ DefinedUseQueryResult,
+ MutationFunction,
+ QueryClient,
+ QueryFunction,
+ QueryKey,
+ UndefinedInitialDataOptions,
+ UseMutationOptions,
+ UseMutationResult,
+ UseQueryOptions,
+ UseQueryResult,
+} from "@tanstack/react-query";
+
+import type {
+ ApiNotificationsListParams,
+ ApiNotificationsReadAllCreate200,
+ ApiNotificationsUnreadCountRetrieve200,
+ Notification,
+ PaginatedNotificationList,
+} from "../models";
+
+import { privateMutator } from "../../../privateClient";
+
+// https://stackoverflow.com/questions/49579094/typescript-conditional-types-filter-out-readonly-properties-pick-only-requir/49579497#49579497
+type IfEquals =
+ (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B;
+
+type WritableKeys = {
+ [P in keyof T]-?: IfEquals<
+ { [Q in P]: T[P] },
+ { -readonly [Q in P]: T[P] },
+ P
+ >;
+}[keyof T];
+
+type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (
+ k: infer I,
+) => void
+ ? I
+ : never;
+type DistributeReadOnlyOverUnions = T extends any ? NonReadonly : never;
+
+type Writable = Pick>;
+type NonReadonly = [T] extends [UnionToIntersection]
+ ? {
+ [P in keyof Writable]: T[P] extends object
+ ? NonReadonly>
+ : T[P];
+ }
+ : DistributeReadOnlyOverUnions;
+
+export const apiNotificationsList = (
+ params?: ApiNotificationsListParams,
+ signal?: AbortSignal,
+) => {
+ return privateMutator({
+ url: `/api/notifications/`,
+ method: "GET",
+ params,
+ signal,
+ });
+};
+
+export const getApiNotificationsListQueryKey = (
+ params?: ApiNotificationsListParams,
+) => {
+ return [`/api/notifications/`, ...(params ? [params] : [])] as const;
+};
+
+export const getApiNotificationsListQueryOptions = <
+ TData = Awaited>,
+ TError = unknown,
+>(
+ params?: ApiNotificationsListParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+) => {
+ const { query: queryOptions } = options ?? {};
+
+ const queryKey =
+ queryOptions?.queryKey ?? getApiNotificationsListQueryKey(params);
+
+ const queryFn: QueryFunction<
+ Awaited>
+ > = ({ signal }) => apiNotificationsList(params, signal);
+
+ return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ > & { queryKey: DataTag };
+};
+
+export type ApiNotificationsListQueryResult = NonNullable<
+ Awaited>
+>;
+export type ApiNotificationsListQueryError = unknown;
+
+export function useApiNotificationsList<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ params: undefined | ApiNotificationsListParams,
+ options: {
+ query: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ > &
+ Pick<
+ DefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ "initialData"
+ >;
+ },
+ queryClient?: QueryClient,
+): DefinedUseQueryResult & {
+ queryKey: DataTag;
+};
+export function useApiNotificationsList<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ params?: ApiNotificationsListParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ > &
+ Pick<
+ UndefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ "initialData"
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+};
+export function useApiNotificationsList<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ params?: ApiNotificationsListParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+};
+
+export function useApiNotificationsList<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ params?: ApiNotificationsListParams,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+} {
+ const queryOptions = getApiNotificationsListQueryOptions(params, options);
+
+ const query = useQuery(queryOptions, queryClient) as UseQueryResult<
+ TData,
+ TError
+ > & { queryKey: DataTag };
+
+ return { ...query, queryKey: queryOptions.queryKey };
+}
+
+export const apiNotificationsRetrieve = (id: string, signal?: AbortSignal) => {
+ return privateMutator({
+ url: `/api/notifications/${id}/`,
+ method: "GET",
+ signal,
+ });
+};
+
+export const getApiNotificationsRetrieveQueryKey = (id: string) => {
+ return [`/api/notifications/${id}/`] as const;
+};
+
+export const getApiNotificationsRetrieveQueryOptions = <
+ TData = Awaited>,
+ TError = unknown,
+>(
+ id: string,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+) => {
+ const { query: queryOptions } = options ?? {};
+
+ const queryKey =
+ queryOptions?.queryKey ?? getApiNotificationsRetrieveQueryKey(id);
+
+ const queryFn: QueryFunction<
+ Awaited>
+ > = ({ signal }) => apiNotificationsRetrieve(id, signal);
+
+ return {
+ queryKey,
+ queryFn,
+ enabled: !!id,
+ ...queryOptions,
+ } as UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ > & { queryKey: DataTag };
+};
+
+export type ApiNotificationsRetrieveQueryResult = NonNullable<
+ Awaited>
+>;
+export type ApiNotificationsRetrieveQueryError = unknown;
+
+export function useApiNotificationsRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ id: string,
+ options: {
+ query: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ > &
+ Pick<
+ DefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ "initialData"
+ >;
+ },
+ queryClient?: QueryClient,
+): DefinedUseQueryResult & {
+ queryKey: DataTag;
+};
+export function useApiNotificationsRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ id: string,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ > &
+ Pick<
+ UndefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ "initialData"
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+};
+export function useApiNotificationsRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ id: string,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+};
+
+export function useApiNotificationsRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ id: string,
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+} {
+ const queryOptions = getApiNotificationsRetrieveQueryOptions(id, options);
+
+ const query = useQuery(queryOptions, queryClient) as UseQueryResult<
+ TData,
+ TError
+ > & { queryKey: DataTag };
+
+ return { ...query, queryKey: queryOptions.queryKey };
+}
+
+/**
+ * @summary Mark a single notification as read
+ */
+export const apiNotificationsReadCreate = (
+ id: string,
+ notification: NonReadonly,
+ signal?: AbortSignal,
+) => {
+ return privateMutator({
+ url: `/api/notifications/${id}/read/`,
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ data: notification,
+ signal,
+ });
+};
+
+export const getApiNotificationsReadCreateMutationOptions = <
+ TError = unknown,
+ TContext = unknown,
+>(options?: {
+ mutation?: UseMutationOptions<
+ Awaited>,
+ TError,
+ { id: string; data: NonReadonly },
+ TContext
+ >;
+}): UseMutationOptions<
+ Awaited>,
+ TError,
+ { id: string; data: NonReadonly },
+ TContext
+> => {
+ const mutationKey = ["apiNotificationsReadCreate"];
+ const { mutation: mutationOptions } = options
+ ? options.mutation &&
+ "mutationKey" in options.mutation &&
+ options.mutation.mutationKey
+ ? options
+ : { ...options, mutation: { ...options.mutation, mutationKey } }
+ : { mutation: { mutationKey } };
+
+ const mutationFn: MutationFunction<
+ Awaited>,
+ { id: string; data: NonReadonly }
+ > = (props) => {
+ const { id, data } = props ?? {};
+
+ return apiNotificationsReadCreate(id, data);
+ };
+
+ return { mutationFn, ...mutationOptions };
+};
+
+export type ApiNotificationsReadCreateMutationResult = NonNullable<
+ Awaited>
+>;
+export type ApiNotificationsReadCreateMutationBody = NonReadonly;
+export type ApiNotificationsReadCreateMutationError = unknown;
+
+/**
+ * @summary Mark a single notification as read
+ */
+export const useApiNotificationsReadCreate = <
+ TError = unknown,
+ TContext = unknown,
+>(
+ options?: {
+ mutation?: UseMutationOptions<
+ Awaited>,
+ TError,
+ { id: string; data: NonReadonly },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): UseMutationResult<
+ Awaited>,
+ TError,
+ { id: string; data: NonReadonly },
+ TContext
+> => {
+ return useMutation(
+ getApiNotificationsReadCreateMutationOptions(options),
+ queryClient,
+ );
+};
+/**
+ * @summary Mark all notifications as read
+ */
+export const apiNotificationsReadAllCreate = (
+ notification: NonReadonly,
+ signal?: AbortSignal,
+) => {
+ return privateMutator({
+ url: `/api/notifications/read-all/`,
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ data: notification,
+ signal,
+ });
+};
+
+export const getApiNotificationsReadAllCreateMutationOptions = <
+ TError = unknown,
+ TContext = unknown,
+>(options?: {
+ mutation?: UseMutationOptions<
+ Awaited>,
+ TError,
+ { data: NonReadonly },
+ TContext
+ >;
+}): UseMutationOptions<
+ Awaited>,
+ TError,
+ { data: NonReadonly },
+ TContext
+> => {
+ const mutationKey = ["apiNotificationsReadAllCreate"];
+ const { mutation: mutationOptions } = options
+ ? options.mutation &&
+ "mutationKey" in options.mutation &&
+ options.mutation.mutationKey
+ ? options
+ : { ...options, mutation: { ...options.mutation, mutationKey } }
+ : { mutation: { mutationKey } };
+
+ const mutationFn: MutationFunction<
+ Awaited>,
+ { data: NonReadonly }
+ > = (props) => {
+ const { data } = props ?? {};
+
+ return apiNotificationsReadAllCreate(data);
+ };
+
+ return { mutationFn, ...mutationOptions };
+};
+
+export type ApiNotificationsReadAllCreateMutationResult = NonNullable<
+ Awaited>
+>;
+export type ApiNotificationsReadAllCreateMutationBody =
+ NonReadonly;
+export type ApiNotificationsReadAllCreateMutationError = unknown;
+
+/**
+ * @summary Mark all notifications as read
+ */
+export const useApiNotificationsReadAllCreate = <
+ TError = unknown,
+ TContext = unknown,
+>(
+ options?: {
+ mutation?: UseMutationOptions<
+ Awaited>,
+ TError,
+ { data: NonReadonly },
+ TContext
+ >;
+ },
+ queryClient?: QueryClient,
+): UseMutationResult<
+ Awaited>,
+ TError,
+ { data: NonReadonly },
+ TContext
+> => {
+ return useMutation(
+ getApiNotificationsReadAllCreateMutationOptions(options),
+ queryClient,
+ );
+};
+/**
+ * @summary Unread notification count
+ */
+export const apiNotificationsUnreadCountRetrieve = (signal?: AbortSignal) => {
+ return privateMutator({
+ url: `/api/notifications/unread-count/`,
+ method: "GET",
+ signal,
+ });
+};
+
+export const getApiNotificationsUnreadCountRetrieveQueryKey = () => {
+ return [`/api/notifications/unread-count/`] as const;
+};
+
+export const getApiNotificationsUnreadCountRetrieveQueryOptions = <
+ TData = Awaited>,
+ TError = unknown,
+>(options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+}) => {
+ const { query: queryOptions } = options ?? {};
+
+ const queryKey =
+ queryOptions?.queryKey ?? getApiNotificationsUnreadCountRetrieveQueryKey();
+
+ const queryFn: QueryFunction<
+ Awaited>
+ > = ({ signal }) => apiNotificationsUnreadCountRetrieve(signal);
+
+ return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ > & { queryKey: DataTag };
+};
+
+export type ApiNotificationsUnreadCountRetrieveQueryResult = NonNullable<
+ Awaited>
+>;
+export type ApiNotificationsUnreadCountRetrieveQueryError = unknown;
+
+export function useApiNotificationsUnreadCountRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ options: {
+ query: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ > &
+ Pick<
+ DefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ "initialData"
+ >;
+ },
+ queryClient?: QueryClient,
+): DefinedUseQueryResult & {
+ queryKey: DataTag;
+};
+export function useApiNotificationsUnreadCountRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ > &
+ Pick<
+ UndefinedInitialDataOptions<
+ Awaited>,
+ TError,
+ Awaited>
+ >,
+ "initialData"
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+};
+export function useApiNotificationsUnreadCountRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+};
+/**
+ * @summary Unread notification count
+ */
+
+export function useApiNotificationsUnreadCountRetrieve<
+ TData = Awaited>,
+ TError = unknown,
+>(
+ options?: {
+ query?: Partial<
+ UseQueryOptions<
+ Awaited>,
+ TError,
+ TData
+ >
+ >;
+ },
+ queryClient?: QueryClient,
+): UseQueryResult & {
+ queryKey: DataTag;
+} {
+ const queryOptions =
+ getApiNotificationsUnreadCountRetrieveQueryOptions(options);
+
+ const query = useQuery(queryOptions, queryClient) as UseQueryResult<
+ TData,
+ TError
+ > & { queryKey: DataTag };
+
+ return { ...query, queryKey: queryOptions.queryKey };
+}
diff --git a/frontend/src/api/generated/public/models/index.ts b/frontend/src/api/generated/public/models/index.ts
index 80f288a..25ef3a6 100644
--- a/frontend/src/api/generated/public/models/index.ts
+++ b/frontend/src/api/generated/public/models/index.ts
@@ -50,6 +50,8 @@ export * from "./messageFile";
export * from "./messageReaction";
export * from "./messageSend";
export * from "./messageSender";
+export * from "./notification";
+export * from "./notificationTypeEnum";
export * from "./orderCarrier";
export * from "./orderCreate";
export * from "./orderItemCreate";
@@ -66,6 +68,7 @@ export * from "./paginatedHubList";
export * from "./paginatedHubPermissionList";
export * from "./paginatedChatList";
export * from "./paginatedMessageList";
+export * from "./paginatedNotificationList";
export * from "./paginatedOrderReadList";
export * from "./paginatedPostList";
export * from "./paginatedProductImageList";
diff --git a/frontend/src/api/generated/public/models/notification.ts b/frontend/src/api/generated/public/models/notification.ts
new file mode 100644
index 0000000..1b97971
--- /dev/null
+++ b/frontend/src/api/generated/public/models/notification.ts
@@ -0,0 +1,36 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+import type { NotificationTypeEnum } from "./notificationTypeEnum";
+
+export interface Notification {
+ readonly id: number;
+ /** Předmět oznámení. */
+ readonly title: string;
+ /** Obsah oznámení. */
+ readonly text: string;
+ /** Kategorie oznámení — používá se pro ikonky a filtrování na frontendu.
+
+ * `system` - Systém
+ * `order` - Objednávka
+ * `payment` - Platba
+ * `social` - Sociální
+ * `chat` - Chat
+ * `advertisement` - Inzerát */
+ readonly notification_type: NotificationTypeEnum;
+ /**
+ * Volitelný odkaz na detail (např. '/objednavky/123/').
+ * @nullable
+ */
+ readonly action_url: string | null;
+ readonly is_read: boolean;
+ /** @nullable */
+ readonly read_at: Date | null;
+ readonly created_at: Date;
+ /** True, pokud bylo oznámení vytvořeno hromadně pro více uživatelů. */
+ readonly bulk: boolean;
+ /** True, pokud byl zároveň odeslán e-mail. */
+ readonly send_email: boolean;
+}
diff --git a/frontend/src/api/generated/public/models/notificationTypeEnum.ts b/frontend/src/api/generated/public/models/notificationTypeEnum.ts
new file mode 100644
index 0000000..ea760eb
--- /dev/null
+++ b/frontend/src/api/generated/public/models/notificationTypeEnum.ts
@@ -0,0 +1,25 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+
+/**
+ * * `system` - Systém
+ * `order` - Objednávka
+ * `payment` - Platba
+ * `social` - Sociální
+ * `chat` - Chat
+ * `advertisement` - Inzerát
+ */
+export type NotificationTypeEnum =
+ (typeof NotificationTypeEnum)[keyof typeof NotificationTypeEnum];
+
+export const NotificationTypeEnum = {
+ system: "system",
+ order: "order",
+ payment: "payment",
+ social: "social",
+ chat: "chat",
+ advertisement: "advertisement",
+} as const;
diff --git a/frontend/src/api/generated/public/models/paginatedNotificationList.ts b/frontend/src/api/generated/public/models/paginatedNotificationList.ts
new file mode 100644
index 0000000..2ce3168
--- /dev/null
+++ b/frontend/src/api/generated/public/models/paginatedNotificationList.ts
@@ -0,0 +1,15 @@
+/**
+ * Generated by orval v8.8.0 🍺
+ * Do not edit manually.
+ * OpenAPI spec version: 0.0.0
+ */
+import type { Notification } from "./notification";
+
+export interface PaginatedNotificationList {
+ count: number;
+ /** @nullable */
+ next?: string | null;
+ /** @nullable */
+ previous?: string | null;
+ results: Notification[];
+}
diff --git a/frontend/src/components/downloader/Downloader.tsx b/frontend/src/components/downloader/Downloader.tsx
index 0ac408c..d62e574 100644
--- a/frontend/src/components/downloader/Downloader.tsx
+++ b/frontend/src/components/downloader/Downloader.tsx
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
import {
apiDownloaderDownloadRetrieve,
} from '@/api/generated/public/downloader';
@@ -20,6 +21,8 @@ const FILE_EXTENSIONS = [
];
export default function Downloader() {
+ const { t } = useTranslation('downloader');
+
const [videoUrl, setVideoUrl] = useState('');
const [videoInfo, setVideoInfo] = useState(null);
const [isLoading, setIsLoading] = useState(false);
@@ -71,6 +74,7 @@ export default function Downloader() {
}
};
}, []);
+
const getAllVideoQualities = () => {
if (!videoInfo?.videos) return [];
const allQualities = new Set();
@@ -121,12 +125,10 @@ export default function Downloader() {
if (info.is_playlist && info.videos) {
setSelectedVideos(info.videos.map((_, index) => index + 1));
}
-
} catch (err: any) {
const errorMessage = err.response?.data?.error || err.message;
setError({ error: errorMessage });
console.error('Retrieve video info error:', err);
-
} finally {
setIsLoading(false);
}
@@ -134,12 +136,12 @@ export default function Downloader() {
async function handleDownload() {
if (!videoUrl) {
- setError({ error: 'Please enter a URL first' });
+ setError({ error: t('pleaseEnterUrl') });
return;
}
setIsDownloading(true);
- setDownloadStatus('Connecting…');
+ setDownloadStatus(t('connectingStatus'));
setDownloadProgress(null);
setError(null);
@@ -158,7 +160,7 @@ export default function Downloader() {
await new Promise((resolve, reject) => {
ws.onopen = () => {
- setDownloadStatus('Starting…');
+ setDownloadStatus(t('startingStatus'));
// Send download parameters
ws.send(JSON.stringify({
url: videoUrl,
@@ -181,36 +183,24 @@ export default function Downloader() {
setDownloadStatus(data.message);
setDownloadProgress(data.percent);
} else if (data.type === 'done') {
-
- setDownloadStatus('Finalizing download...');
+ setDownloadStatus(t('finalizing'));
setDownloadProgress(100);
-
- // Přidáme responseType: 'blob', aby Axios/Fetch vrátil soubor správně
- publicApi.post('/api/downloader/download/file/',
- { token: data.token },
+
+ publicApi.post('/api/downloader/download/file/',
+ { token: data.token },
{ responseType: 'blob' }
)
.then((response) => {
- // 1. Vytvoření dočasné URL z přijatého Blobu
const url = window.URL.createObjectURL(new Blob([response.data]));
-
- // 2. Extrakce názvu souboru z hlaviček (pokud ho backend posílá)
let filename = data.filename || `video.${selectedExtension}`;
-
- // 3. Vytvoření neviditelného odkazu pro spuštění stahování
const link = document.createElement('a');
link.href = url;
- link.setAttribute('download', filename); // Zajišťuje stáhnutí místo otevření
+ link.setAttribute('download', filename);
document.body.appendChild(link);
-
- // 4. Simulace kliknutí
link.click();
-
- // 5. Úklid po stažení
link.remove();
window.URL.revokeObjectURL(url);
-
- setDownloadStatus('Download complete!');
+ setDownloadStatus(t('downloadComplete'));
resolve();
})
.catch((err) => {
@@ -251,18 +241,18 @@ export default function Downloader() {
return (
-
Video Downloader
+
{t('title')}
@@ -272,12 +262,12 @@ export default function Downloader() {
disabled={isLoading || !videoUrl}
className="px-6 py-3 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed mx-2 mb-4"
>
- {isLoading ? 'Loading...' : 'Retrieve Options'}
+ {isLoading ? t('loading') : t('retrieveOptions')}
{error && (
- Error: {error.error}
+ {t('error')}: {error.error}
)}
@@ -286,28 +276,28 @@ export default function Downloader() {
{videoInfo.is_playlist ? (
- 📋 {videoInfo.playlist_title || 'Playlist'}
+ 📋 {videoInfo.playlist_title || t('playlist')}
- {videoInfo.playlist_count} videos found
+ {t('videosFound', { count: videoInfo.playlist_count })}
{/* Playlist Video Selection */}
-
Select Videos to Download:
+
{t('selectVideos')}
@@ -338,12 +328,12 @@ export default function Downloader() {
{video.duration && (
- Duration: {Math.floor(video.duration / 60)}:{String(video.duration % 60).padStart(2, '0')}
+ {t('duration')}: {Math.floor(video.duration / 60)}:{String(video.duration % 60).padStart(2, '0')}
)}
- Quality: {video.video_resolutions.join(', ') || 'N/A'} |
- Audio: {video.audio_resolutions.join(', ') || 'N/A'}
+ {t('quality')}: {video.video_resolutions.join(', ') || 'N/A'} |
+ {t('audio')}: {video.audio_resolutions.join(', ') || 'N/A'}
@@ -354,7 +344,7 @@ export default function Downloader() {
- {selectedVideos.length} of {videoInfo.videos.length} videos selected
+ {t('selected', { selected: selectedVideos.length, total: videoInfo.videos.length })}
@@ -374,7 +364,7 @@ export default function Downloader() {
{videoInfo.videos[0]?.duration && (
- Duration: {Math.floor(videoInfo.videos[0].duration / 60)}:{String(videoInfo.videos[0].duration % 60).padStart(2, '0')}
+ {t('duration')}: {Math.floor(videoInfo.videos[0].duration / 60)}:{String(videoInfo.videos[0].duration % 60).padStart(2, '0')}
)}
@@ -386,14 +376,14 @@ export default function Downloader() {
diff --git a/frontend/src/components/home/Footer/footer.tsx b/frontend/src/components/home/Footer/footer.tsx
index 492ac17..03bdfce 100644
--- a/frontend/src/components/home/Footer/footer.tsx
+++ b/frontend/src/components/home/Footer/footer.tsx
@@ -4,6 +4,7 @@ import {
} from "react-icons/fa6";
import { FaPhoneAlt, FaEnvelope, FaTeamspeak, FaGlobe } from "react-icons/fa";
import { GiAutoRepair } from "react-icons/gi";
+import { useTranslation } from "react-i18next";
const SOCIALS = [
{ Icon: FaGitAlt, href: "https://git.vontor.cz/Brunobrno", label: "Gitea" },
@@ -20,18 +21,20 @@ const CONTACTS = [
{ Icon: FaTeamspeak, href: undefined, label: "teamspeak.vontor.cz:4926" },
];
-const SERVICES = [
- { Icon: FaGlobe, href: "/services/web", label: "Weby" },
- { Icon: FaClapperboard, href: "/services/film", label: "Filmařina" },
- { Icon: GiAutoRepair, href: "/services/drone", label: "Servis dronu" },
-];
-
const fade = {
initial: { opacity: 0, y: 24 },
- animate: { opacity: 1, y: 0, transition: { duration: 0.6, ease: "easeOut" } },
+ animate: { opacity: 1, y: 0, transition: { duration: 0.6, ease: "easeOut" as const } },
};
export default function Footer() {
+ const { t } = useTranslation("home");
+
+ const SERVICES = [
+ { Icon: FaGlobe, href: "/services/web", label: t("footer.serviceWeb") },
+ { Icon: FaClapperboard, href: "/services/film", label: t("footer.serviceFilm") },
+ { Icon: GiAutoRepair, href: "/services/drone", label: t("footer.serviceDrone") },
+ ];
+
return (
@@ -91,7 +94,7 @@ export default function Footer() {
textTransform: "uppercase", color: "var(--c-other)",
margin: "0 0 1.2rem",
}}>
- Kontakty
+ {t("footer.contactsHeading")}
{CONTACTS.map(({ Icon, href, label }) => (
@@ -122,7 +125,7 @@ export default function Footer() {
textTransform: "uppercase", color: "var(--c-other)",
margin: "0 0 1.2rem",
}}>
- Služby
+ {t("footer.servicesHeading")}