feat: polish
This commit is contained in:
parent
2e6177fe74
commit
4bd927bb4e
137 changed files with 6357 additions and 137560 deletions
22
src/routes/+layout.svelte
Normal file
22
src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { initI18n } from '$i18n';
|
||||
import { authStore } from '$auth/store';
|
||||
import ToastContainer from '$ui/Toast.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let ready = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
await initI18n();
|
||||
await authStore.refresh().catch(() => {});
|
||||
ready = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if ready}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
<ToastContainer />
|
||||
5
src/routes/+layout.ts
Normal file
5
src/routes/+layout.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Pure SPA: all rendering happens in the browser against a Django REST backend.
|
||||
// Disable SSR and prerender globally so every route is just a client-side chunk.
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
export const trailingSlash = 'ignore';
|
||||
|
|
@ -1,7 +1,22 @@
|
|||
<script>
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$auth';
|
||||
|
||||
onMount(() => {
|
||||
// Root route funnels users into the predict workspace by default. Once
|
||||
// we have a proper landing page this can render marketing content
|
||||
// instead of redirecting.
|
||||
const unsub = authStore.subscribe((state) => {
|
||||
if (state.status === 'authenticated') goto('/predict');
|
||||
else if (state.status === 'anonymous') goto('/login');
|
||||
});
|
||||
return () => unsub();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Navbar />
|
||||
</main>
|
||||
<div class="d-flex justify-content-center align-items-center" style="height: 100vh;">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ssr =false;
|
||||
|
|
@ -1,97 +1,5 @@
|
|||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { login } from '$lib/auth';
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
let error = '';
|
||||
let isLoading = false;
|
||||
|
||||
async function handleLogin() {
|
||||
if (!username || !password) {
|
||||
error = 'Please enter both username and password';
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
error = '';
|
||||
|
||||
console.log("Sending request:", username, password);
|
||||
|
||||
// login request
|
||||
try {
|
||||
await login(username, password);
|
||||
|
||||
goto('/'); // Redirect after successful login
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
error = err.message || 'Invalid credentials';
|
||||
} else {
|
||||
error = 'Invalid credentials';
|
||||
}
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
<script lang="ts">
|
||||
import { LoginForm } from '$features/auth';
|
||||
</script>
|
||||
|
||||
|
||||
<main class="container pt-3">
|
||||
<div class="text-center mt-5 mb-4">
|
||||
<img src="/logo-lg.svg" alt="ООО ЯКС" width="300" class="rounded-3" />
|
||||
<h2 class="text-center mt-4 mb-5">Стратосферные полеты | ООО ЯКС</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-4 offset-md-3 offset-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Вход в учетную запись</h5>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-danger mb-4" role="alert">{error}</div>
|
||||
{/if}
|
||||
|
||||
<form on:submit|preventDefault={handleLogin} class="mt-4">
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="username"
|
||||
placeholder="Имя пользователя"
|
||||
bind:value={username}
|
||||
required
|
||||
/>
|
||||
<label for="username">Имя пользователя</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder="Пароль"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
<label for="password">Пароль</label>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{#if isLoading}
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Вход...
|
||||
{:else}
|
||||
Войти
|
||||
{/if}
|
||||
</button>
|
||||
<a href="/" class="btn btn-secondary mt-3 w-100">Назад</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<LoginForm />
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ssr =false;
|
||||
|
|
@ -1,130 +1,113 @@
|
|||
<script lang="ts">
|
||||
import Map from "$lib/components/Map.svelte";
|
||||
import ControlPanel from "$lib/components/ControlPanel.svelte";
|
||||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import PanelContainer from "$lib/components/PanelContainer.svelte";
|
||||
import ScenarioPanel from "$lib/components/ScenarioPanel.svelte";
|
||||
import TabComponent from "$lib/components/ui/TabComponent.svelte";
|
||||
import PointEditor from "$lib/components/editors/PointEditor.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { PredictionStore } from "$lib/stores";
|
||||
import { addToast, removeToast } from "$lib/components/ui/Toast.svelte";
|
||||
import ToastContainer from '$lib/components/ui/Toast.svelte';
|
||||
import GenericPanel from "$lib/components/GenericPanel.svelte";
|
||||
import TimeLine from "$lib/components/TimeLine.svelte";
|
||||
import { onMount } from 'svelte';
|
||||
import { Map as MapView } from '$map';
|
||||
import type { IMap } from '$map';
|
||||
import { startCoordinateSelection } from '$map';
|
||||
import { Navbar } from '$features/auth';
|
||||
import { PanelContainer, TabBar } from '$ui';
|
||||
import { addToast, removeToast } from '$ui';
|
||||
import { ControlPanel, ScenarioPanel } from '$features/prediction';
|
||||
import {
|
||||
WorkspacesPanel,
|
||||
WorkspaceRenderer,
|
||||
workspacesStore,
|
||||
} from '$features/workspaces';
|
||||
import { SettingsPanel } from '$features/settings';
|
||||
import { TimeLine } from '$features/timeline';
|
||||
import { t } from '$i18n';
|
||||
import { requireAuthenticated } from '$auth';
|
||||
|
||||
let map: Map | null = null;
|
||||
let panelContainer: PanelContainer | null = null;
|
||||
let controlPanel: ControlPanel | null = null;
|
||||
let selectionToastId: string | null = null;
|
||||
let activeTabLeft: 'control' | 'scenario' | 'about' = 'scenario';
|
||||
let activeTabRight: 'layers' | 'settings' | 'results' = 'results';
|
||||
type LeftTab = 'scenario' | 'conditions' | 'about';
|
||||
type RightTab = 'results' | 'workspaces' | 'settings';
|
||||
|
||||
onMount(() => {
|
||||
PredictionStore.subscribe((data) => {
|
||||
if (data) {
|
||||
map?.clearMapLayers();
|
||||
}
|
||||
});
|
||||
console.log("ControlPanel mounted");
|
||||
console.log(panelContainer);
|
||||
let mapComponent = $state<MapView | null>(null);
|
||||
let controlPanelRef = $state<ControlPanel | null>(null);
|
||||
let selectionToastId: string | null = null;
|
||||
let selectionDispose: (() => void) | null = null;
|
||||
|
||||
if (panelContainer) {
|
||||
let element = panelContainer.getElement();
|
||||
if (!element) return;
|
||||
let leftTab = $state<LeftTab>('scenario');
|
||||
let rightTab = $state<RightTab>('workspaces');
|
||||
|
||||
// Disable click and scroll propagation to prevent map interaction
|
||||
element.addEventListener('click', (e) => e.stopPropagation());
|
||||
element.addEventListener('dblclick', (e) => e.stopPropagation());
|
||||
element.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||
element.addEventListener('touchstart', (e) => e.stopPropagation());
|
||||
element.addEventListener('wheel', (e) => e.stopPropagation());
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
const ok = await requireAuthenticated('/login');
|
||||
if (!ok) return;
|
||||
if ($workspacesStore.items.length === 0) {
|
||||
workspacesStore.add({ name: $t('workspaces.defaultName', { n: 1 }) });
|
||||
}
|
||||
});
|
||||
|
||||
function handleClickSelectOnMap() {
|
||||
if (map) {
|
||||
map.startSelection();
|
||||
console.log("Selection mode enabled");
|
||||
if (!selectionToastId) {
|
||||
selectionToastId = addToast({
|
||||
header: "Режим выбора координат",
|
||||
body: "Кликните на карту, чтобы выбрать координаты",
|
||||
color: "info",
|
||||
persistent: true,
|
||||
onRemoveCallback: () => {
|
||||
selectionToastId = null;
|
||||
map?.stopSelection();
|
||||
console.log("Selection mode disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleMapReady(_map: IMap) {
|
||||
// Map is ready; consumers inside <MapView /> already have access via context.
|
||||
}
|
||||
|
||||
function handleCoordinateSelection(event: CustomEvent<{ lat: number; lng: number }>) {
|
||||
const { lat, lng } = event.detail;
|
||||
controlPanel?.updateLaunchPosition(lat, lng);
|
||||
console.log(`Selected coordinates: ${lat}, ${lng}`);
|
||||
if (selectionToastId) {
|
||||
removeToast(selectionToastId);
|
||||
selectionToastId = null;
|
||||
}
|
||||
}
|
||||
function handleSelectOnMap() {
|
||||
const map = mapComponent?.getInstance();
|
||||
if (!map) return;
|
||||
|
||||
function handleTimeUpdate(event: CustomEvent<{ index: number; lat: number; lng: number; alt: number; datetime: Date }>) {
|
||||
const { lat, lng } = event.detail;
|
||||
map?.updateAnimatedMarker(lat, lng);
|
||||
}
|
||||
selectionDispose = startCoordinateSelection(map, ({ lat, lng }) => {
|
||||
controlPanelRef?.updateLaunchPosition(lat, lng);
|
||||
if (selectionToastId) {
|
||||
removeToast(selectionToastId);
|
||||
selectionToastId = null;
|
||||
}
|
||||
selectionDispose = null;
|
||||
});
|
||||
|
||||
selectionToastId = addToast({
|
||||
header: $t('selection.header'),
|
||||
body: $t('selection.body'),
|
||||
color: 'info',
|
||||
persistent: true,
|
||||
onRemove: () => {
|
||||
selectionDispose?.();
|
||||
selectionDispose = null;
|
||||
selectionToastId = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div> <!-- Spacer for fixed navbar -->
|
||||
<Map bind:this={map} mode="prediction" bind:data={$PredictionStore} on:coordinatesSelected={handleCoordinateSelection}>
|
||||
<PanelContainer bind:this={panelContainer} position="left">
|
||||
<TabComponent
|
||||
tabs={[
|
||||
{ id: 'scenario', icon: 'file-earmark-play', label: 'Сценарий' },
|
||||
{ id: 'control', icon: 'sliders', label: 'Условия' },
|
||||
{ id: 'about', icon: 'info-circle', label: 'О проекте' }
|
||||
]}
|
||||
bind:activeTab={activeTabLeft}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{#if activeTabLeft === 'control'}
|
||||
<ControlPanel onSelectOnMapClick={handleClickSelectOnMap} bind:this={controlPanel} />
|
||||
{:else if activeTabLeft === 'scenario'}
|
||||
<ScenarioPanel />
|
||||
{:else if activeTabLeft === 'about'}
|
||||
<!-- <AboutPanel /> -->
|
||||
{/if}
|
||||
</div>
|
||||
</PanelContainer>
|
||||
<PanelContainer position="right">
|
||||
<TabComponent
|
||||
justify="end"
|
||||
tabs={[
|
||||
{ id: 'results', icon: 'bar-chart-line', label: 'Результаты' },
|
||||
{ id: 'layers', icon: 'layers', label: 'Слои' },
|
||||
{ id: 'settings', icon: 'gear', label: 'Настройки' },
|
||||
]}
|
||||
bind:activeTab={activeTabRight}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{#if activeTabRight === 'results'}
|
||||
<GenericPanel />
|
||||
{:else if activeTabRight === 'layers'}
|
||||
<GenericPanel />
|
||||
{/if}
|
||||
</div>
|
||||
</PanelContainer>
|
||||
<ToastContainer />
|
||||
{#if $PredictionStore}
|
||||
<TimeLine prediction={$PredictionStore} on:timeUpdate={handleTimeUpdate} />
|
||||
{/if}
|
||||
</Map>
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<MapView bind:this={mapComponent} onReady={handleMapReady}>
|
||||
<WorkspaceRenderer />
|
||||
|
||||
<PanelContainer position="left">
|
||||
<TabBar
|
||||
tabs={[
|
||||
{ id: 'scenario', icon: 'file-earmark-play', label: $t('panel.scenario') },
|
||||
{ id: 'conditions', icon: 'sliders', label: $t('panel.conditions') },
|
||||
{ id: 'about', icon: 'info-circle', label: $t('panel.about') },
|
||||
]}
|
||||
bind:active={leftTab} />
|
||||
<div>
|
||||
{#if leftTab === 'scenario'}
|
||||
<ScenarioPanel />
|
||||
{:else if leftTab === 'conditions'}
|
||||
<ControlPanel bind:this={controlPanelRef} onSelectOnMapClick={handleSelectOnMap} />
|
||||
{/if}
|
||||
</div>
|
||||
</PanelContainer>
|
||||
|
||||
<PanelContainer position="right">
|
||||
<TabBar
|
||||
justify="end"
|
||||
tabs={[
|
||||
{ id: 'workspaces', icon: 'layers', label: $t('panel.workspaces') },
|
||||
{ id: 'results', icon: 'bar-chart-line', label: $t('panel.results') },
|
||||
{ id: 'settings', icon: 'gear', label: $t('panel.settings') },
|
||||
]}
|
||||
bind:active={rightTab} />
|
||||
<div>
|
||||
{#if rightTab === 'workspaces'}
|
||||
<WorkspacesPanel />
|
||||
{:else if rightTab === 'settings'}
|
||||
<SettingsPanel />
|
||||
{/if}
|
||||
</div>
|
||||
</PanelContainer>
|
||||
|
||||
<TimeLine />
|
||||
</MapView>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ssr =false;
|
||||
130432
src/routes/testVelo.json
130432
src/routes/testVelo.json
File diff suppressed because it is too large
Load diff
|
|
@ -1,20 +1,22 @@
|
|||
<script>
|
||||
import Map from '$lib/components/Map.svelte';
|
||||
import TelemetryPanel from '$lib/components/TelemetryPanel.svelte';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
// import BurstCalculator from './BurstCalculator.svelte';
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Map as MapView } from '$map';
|
||||
import { Navbar } from '$features/auth';
|
||||
import { PanelContainer } from '$ui';
|
||||
import { TelemetryPanel } from '$features/tracking';
|
||||
import { requireAuthenticated } from '$auth';
|
||||
|
||||
let coordinates = {
|
||||
lat: '56.3576',
|
||||
lng: '39.8666'
|
||||
}
|
||||
onMount(() => {
|
||||
requireAuthenticated('/login');
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div> <!-- Spacer for fixed navbar -->
|
||||
<Map>
|
||||
<TelemetryPanel
|
||||
/>
|
||||
</Map>
|
||||
</main>
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<MapView>
|
||||
<PanelContainer position="left">
|
||||
<TelemetryPanel />
|
||||
</PanelContainer>
|
||||
</MapView>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ssr =false;
|
||||
|
|
@ -1,267 +1,84 @@
|
|||
<script lang="ts">
|
||||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Button,
|
||||
FormGroup,
|
||||
Label,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Button,
|
||||
FormGroup,
|
||||
Label,
|
||||
Input,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
import { Navbar } from '$features/auth';
|
||||
import { Footer } from '$features/footer';
|
||||
import { ConfirmationPrompt } from '$ui';
|
||||
import { authStore, requireAuthenticated } from '$auth';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$i18n';
|
||||
|
||||
import ConfirmationPrompt from "$lib/components/ConfirmationPrompt.svelte";
|
||||
import Footer from "$lib/components/Footer.svelte";
|
||||
let showConfirm = $state(false);
|
||||
let confirmTitle = $state('');
|
||||
let confirmBody = $state('');
|
||||
let confirmAction = $state<() => void>(() => {});
|
||||
|
||||
let editMode = false;
|
||||
let showToken = false;
|
||||
onMount(async () => {
|
||||
if (!(await requireAuthenticated('/login'))) return;
|
||||
});
|
||||
|
||||
type ConfirmConfig = {
|
||||
title: string;
|
||||
body: string;
|
||||
confirmText: string;
|
||||
confirmVariant?: string;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
function ask(title: string, body: string, action: () => void) {
|
||||
confirmTitle = title;
|
||||
confirmBody = body;
|
||||
confirmAction = action;
|
||||
showConfirm = true;
|
||||
}
|
||||
|
||||
// State for the single confirmation prompt
|
||||
let showConfirm = false;
|
||||
let confirmConfig: ConfirmConfig = {
|
||||
title: "",
|
||||
body: "",
|
||||
confirmText: "",
|
||||
confirmVariant: "primary",
|
||||
onConfirm: () => {},
|
||||
};
|
||||
|
||||
function openConfirmation(config: Partial<ConfirmConfig>) {
|
||||
confirmConfig = { ...confirmConfig, ...config } as ConfirmConfig;
|
||||
showConfirm = true;
|
||||
}
|
||||
|
||||
function handleDeleteAccount() {
|
||||
openConfirmation({
|
||||
title: "Подтвердите удаление",
|
||||
body: "Вы уверены, что хотите удалить свою учетную запись? Это действие необратимо.",
|
||||
confirmText: "Удалить",
|
||||
confirmVariant: "danger",
|
||||
onConfirm: confirmDeleteAccount,
|
||||
});
|
||||
}
|
||||
|
||||
function handleResetSettings() {
|
||||
openConfirmation({
|
||||
title: "Подтвердите сброс",
|
||||
body: "Вы уверены, что хотите сбросить учетную запись? Это также удалит все сохранные сценарии, шаблоны и точки запуска.",
|
||||
confirmText: "Сбросить",
|
||||
confirmVariant: "warning",
|
||||
onConfirm: confirmResetSettings,
|
||||
});
|
||||
}
|
||||
|
||||
function handleGenerateToken() {
|
||||
openConfirmation({
|
||||
title: "Подтвердите создание токена",
|
||||
body: "Генерация нового токена API приведет к прекращению действия старого токена. Приложения, использующие старый токен, перестанут работать. Вы уверены, что хотите создать новый токен?",
|
||||
confirmText: "Создать",
|
||||
confirmVariant: "primary",
|
||||
onConfirm: confirmGenerateToken,
|
||||
});
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
openConfirmation({
|
||||
title: "Подтвердите выход",
|
||||
body: "Вы уверены, что хотите выйти из учетной записи? Вы будете перенаправлены на страницу входа.",
|
||||
confirmText: "Выйти",
|
||||
onConfirm: confirmLogout,
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDeleteAccount() {
|
||||
// Implement account deletion logic
|
||||
console.log("Account deleted");
|
||||
}
|
||||
|
||||
function confirmResetSettings() {
|
||||
// Implement settings reset logic
|
||||
console.log("Settings reset");
|
||||
}
|
||||
|
||||
function confirmGenerateToken() {
|
||||
// Implement token generation logic
|
||||
console.log("New token generated");
|
||||
}
|
||||
|
||||
function confirmLogout() {
|
||||
// Implement logout logic
|
||||
console.log("Logged out");
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (confirmConfig.onConfirm) {
|
||||
confirmConfig.onConfirm();
|
||||
}
|
||||
showConfirm = false;
|
||||
}
|
||||
async function handleLogout() {
|
||||
ask($t('nav.logout'), '', async () => {
|
||||
await authStore.logout();
|
||||
goto('/');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="force-page-height">
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<!-- Spacer for fixed navbar -->
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<!-- Side Navigation -->
|
||||
<div class="col-md-3 col-lg-2 mb-4">
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link active" href="/user/account">Учетная запись</a>
|
||||
<a class="nav-link" href="/user/templates">Сохраненные сценарии</a>
|
||||
<a class="nav-link" href="#api-tokens">История прогнозов</a>
|
||||
<a class="nav-link" href="#actions">История слежения</a>
|
||||
</nav>
|
||||
</div>
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-2 mb-4">
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link active" href="/user/account">{$t('nav.account')}</a>
|
||||
<a class="nav-link" href="/user/templates">{$t('nav.scenarios')}</a>
|
||||
<a class="nav-link" href="/user/predictions">{$t('nav.predictionHistory')}</a>
|
||||
<a class="nav-link" href="/user/flights">{$t('nav.trackingHistory')}</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<!-- Account Information -->
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Основная информация</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<FormGroup>
|
||||
<Label for="username">Имя пользователя:</Label>
|
||||
<Input id="username" value="user123" readonly disabled />
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<FormGroup>
|
||||
<Label for="email">Email:</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value="user@example.com"
|
||||
readonly={!editMode}
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</div>
|
||||
<FormGroup>
|
||||
<Label for="fullname">Полное имя:</Label>
|
||||
<Input id="fullname" value="Иван Иванов" readonly={!editMode} disabled={!editMode} />
|
||||
</FormGroup>
|
||||
{#if editMode}
|
||||
<Button color="success" on:click={() => (editMode = false)}>Сохранить</Button>
|
||||
<Button color="secondary" on:click={() => (editMode = false)}>Отменить</Button>
|
||||
{:else}
|
||||
<Button color="primary" on:click={() => (editMode = true)}>Редактировать</Button>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<!-- Password Change -->
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Смена пароля</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<FormGroup>
|
||||
<Label for="currentPassword">Текущий пароль:</Label>
|
||||
<Input id="currentPassword" type="password" />
|
||||
</FormGroup>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<FormGroup>
|
||||
<Label for="newPassword">Новый пароль:</Label>
|
||||
<Input id="newPassword" type="password" />
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<FormGroup>
|
||||
<Label for="confirmPassword">Повтор пароля:</Label>
|
||||
<Input id="confirmPassword" type="password" />
|
||||
</FormGroup>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="primary">Изменить пароль</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<!-- API Token -->
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Токен API</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<FormGroup>
|
||||
<Label for="apiToken">Токен доступа</Label>
|
||||
<InputGroup>
|
||||
<div class="position-relative flex-grow-1">
|
||||
<Input
|
||||
id="apiToken"
|
||||
class="form-control pe-5"
|
||||
type={showToken ? "text" : "password"}
|
||||
value="abc123def456..."
|
||||
readonly
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y me-2 rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 32px; height: 32px; border: none; color: var(--bs-secondary); z-index: 10;"
|
||||
on:click={() => {
|
||||
showToken = !showToken;
|
||||
}}
|
||||
>
|
||||
<Icon name={showToken ? "eye-slash" : "eye"} style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button>
|
||||
<Icon name="clipboard" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<Button color="warning" on:click={handleGenerateToken}>Сгенерировать новый токен</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<!-- Account Actions -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Действия с аккаунтом</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<Button color="secondary" on:click={handleLogout}>Выйти</Button>
|
||||
<!-- spacer -->
|
||||
<span class="d-none d-md-inline-block flex-grow-1"></span>
|
||||
<Button color="warning" on:click={handleResetSettings}>Сбросить настройки</Button>
|
||||
<Button color="danger" on:click={handleDeleteAccount}>Удалить аккаунт</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<Card class="mb-4">
|
||||
<CardHeader><h5 class="mb-0">{$t('nav.account')}</h5></CardHeader>
|
||||
<CardBody>
|
||||
<FormGroup>
|
||||
<Label>{$t('login.username')}</Label>
|
||||
<Input value={$authStore.username ?? ''} readonly disabled />
|
||||
</FormGroup>
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<Button color="secondary" on:click={handleLogout}>{$t('nav.logout')}</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
<!-- Single Dynamic Confirmation Prompt -->
|
||||
<ConfirmationPrompt
|
||||
bind:isOpen={showConfirm}
|
||||
title={confirmConfig.title}
|
||||
confirmText={confirmConfig.confirmText}
|
||||
confirmVariant={confirmConfig.confirmVariant || "primary"}
|
||||
cancelText="Отмена"
|
||||
onconfirm={handleConfirm}
|
||||
oncancel={() => (showConfirm = false)}
|
||||
>
|
||||
<p>{confirmConfig.body}</p>
|
||||
bind:isOpen={showConfirm}
|
||||
title={confirmTitle}
|
||||
confirmText={$t('editor.save')}
|
||||
cancelText={$t('editor.cancel')}
|
||||
onconfirm={confirmAction}
|
||||
oncancel={() => (showConfirm = false)}>
|
||||
<p>{confirmBody}</p>
|
||||
</ConfirmationPrompt>
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ssr =false;
|
||||
36
src/routes/user/flights/+page.svelte
Normal file
36
src/routes/user/flights/+page.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Card, CardHeader, CardBody } from '@sveltestrap/sveltestrap';
|
||||
import { Navbar } from '$features/auth';
|
||||
import { Footer } from '$features/footer';
|
||||
import { requireAuthenticated } from '$auth';
|
||||
import { t } from '$i18n';
|
||||
|
||||
onMount(() => requireAuthenticated('/login'));
|
||||
</script>
|
||||
|
||||
<main class="force-page-height">
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-2 mb-4">
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link" href="/user/account">{$t('nav.account')}</a>
|
||||
<a class="nav-link" href="/user/templates">{$t('nav.scenarios')}</a>
|
||||
<a class="nav-link" href="/user/predictions">{$t('nav.predictionHistory')}</a>
|
||||
<a class="nav-link active" href="/user/flights">{$t('nav.trackingHistory')}</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<Card>
|
||||
<CardHeader><h5 class="mb-0">{$t('nav.trackingHistory')}</h5></CardHeader>
|
||||
<CardBody>
|
||||
<p class="text-muted small mb-0">TODO: wire to tracking history endpoint.</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const ssr =false;
|
||||
36
src/routes/user/predictions/+page.svelte
Normal file
36
src/routes/user/predictions/+page.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Card, CardHeader, CardBody } from '@sveltestrap/sveltestrap';
|
||||
import { Navbar } from '$features/auth';
|
||||
import { Footer } from '$features/footer';
|
||||
import { requireAuthenticated } from '$auth';
|
||||
import { t } from '$i18n';
|
||||
|
||||
onMount(() => requireAuthenticated('/login'));
|
||||
</script>
|
||||
|
||||
<main class="force-page-height">
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-2 mb-4">
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link" href="/user/account">{$t('nav.account')}</a>
|
||||
<a class="nav-link" href="/user/templates">{$t('nav.scenarios')}</a>
|
||||
<a class="nav-link active" href="/user/predictions">{$t('nav.predictionHistory')}</a>
|
||||
<a class="nav-link" href="/user/flights">{$t('nav.trackingHistory')}</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<Card>
|
||||
<CardHeader><h5 class="mb-0">{$t('nav.predictionHistory')}</h5></CardHeader>
|
||||
<CardBody>
|
||||
<p class="text-muted small mb-0">TODO: wire to prediction history endpoint.</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const ssr =false;
|
||||
|
|
@ -1,379 +1,145 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { TableHandler } from "@vincjo/datatables";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Button,
|
||||
Input,
|
||||
Icon,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from 'svelte';
|
||||
import { TableHandler } from '@vincjo/datatables';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Button,
|
||||
Input,
|
||||
Icon,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
import { Navbar } from '$features/auth';
|
||||
import { Footer } from '$features/footer';
|
||||
import { ConfirmationPrompt, addToast } from '$ui';
|
||||
import { PointEditor, pointsStore } from '$features/prediction';
|
||||
import { pointsApi } from '$api';
|
||||
import type { SavedPoint } from '$domain';
|
||||
import { requireAuthenticated } from '$auth';
|
||||
import { t } from '$i18n';
|
||||
|
||||
import Navbar from "$lib/components/Navbar.svelte";
|
||||
import Footer from "$lib/components/Footer.svelte";
|
||||
import ConfirmationPrompt from "$lib/components/ConfirmationPrompt.svelte";
|
||||
import PointEditor from "$lib/components/editors/PointEditor.svelte";
|
||||
import ToastContainer from "$lib/components/ui/Toast.svelte";
|
||||
import { addToast } from "$lib/components/ui/Toast.svelte";
|
||||
let editPoint = $state<SavedPoint | null>(null);
|
||||
let toDelete = $state<SavedPoint | null>(null);
|
||||
|
||||
// TODO: Implement these imports
|
||||
import { SavedPointsStore, SavedFlightProfilesStore, SavedScenarioStore } from "$lib/stores";
|
||||
import { getSavedPoints, deletePoint } from "$lib/api/points";
|
||||
import { getSavedFlightProfiles, deleteFlightProfile } from "$lib/api/profiles";
|
||||
import { getSavedScenarios, deleteScenario } from "$lib/api/scenarios";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenario } from "$lib/types";
|
||||
const table = $derived(new TableHandler($pointsStore, { rowsPerPage: 5 }));
|
||||
const search = $derived(table.createSearch(['name']));
|
||||
|
||||
// Table handlers
|
||||
let pointsTable = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 5 }));
|
||||
let pointsSearch = $derived(pointsTable.createSearch(["name"]));
|
||||
onMount(async () => {
|
||||
if (!(await requireAuthenticated('/login'))) return;
|
||||
try {
|
||||
pointsStore.set(await pointsApi.list());
|
||||
} catch (err: unknown) {
|
||||
addToast({ header: $t('common.error'), body: (err as Error).message, color: 'danger' });
|
||||
}
|
||||
});
|
||||
|
||||
let profilesTable = $derived(new TableHandler($SavedFlightProfilesStore, { rowsPerPage: 5 }));
|
||||
let profilesSearch = $derived(profilesTable.createSearch(["name"]));
|
||||
|
||||
let templatesTable = $derived(new TableHandler($SavedScenarioStore, { rowsPerPage: 5 }));
|
||||
let templatesSearch = $derived(templatesTable.createSearch(["name"]));
|
||||
|
||||
let editPoint: SavedPoint | null = $state(null);
|
||||
|
||||
onMount(async () => {
|
||||
// Mock data for demonstration. Replace with API calls.
|
||||
const pts = await getSavedPoints();
|
||||
$SavedPointsStore = pts;
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
|
||||
$SavedFlightProfilesStore = [
|
||||
{ id: 1, name: "Standard Weather Balloon", rate_profile_data: {ascent_rate: 5, descent_rate: 8, burst_altitude: 30000} },
|
||||
{ id: 2, name: "High Altitude Probe", rate_profile_data: {ascent_rate: 6, descent_rate: 10, burst_altitude: 40000} },
|
||||
];
|
||||
|
||||
|
||||
/*
|
||||
// TODO: Uncomment when API is ready
|
||||
const [points, profiles, templates] = await Promise.all([
|
||||
getSavedPoints(),
|
||||
getSavedFlightProfiles(),
|
||||
getSavedScenarioTemplates()
|
||||
]);
|
||||
$SavedPointsStore = points;
|
||||
$SavedFlightProfilesStore = profiles;
|
||||
$SavedScenarioTemplatesStore = templates;
|
||||
*/
|
||||
});
|
||||
|
||||
// --- Confirmation Prompt Logic ---
|
||||
type ConfirmConfig = {
|
||||
title: string;
|
||||
body: string;
|
||||
confirmText: string;
|
||||
confirmVariant?: string;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
let showConfirm = $state(false);
|
||||
let confirmConfig = $state<ConfirmConfig>({
|
||||
title: "",
|
||||
body: "",
|
||||
confirmText: "",
|
||||
onConfirm: () => {},
|
||||
});
|
||||
|
||||
function openConfirmation(config: Partial<ConfirmConfig>) {
|
||||
confirmConfig = { ...confirmConfig, ...config } as ConfirmConfig;
|
||||
showConfirm = true;
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (confirmConfig.onConfirm) {
|
||||
confirmConfig.onConfirm();
|
||||
}
|
||||
showConfirm = false;
|
||||
}
|
||||
|
||||
// --- Delete Handlers ---
|
||||
function handleDelete<T extends { id: number; name: string }>(
|
||||
item: T,
|
||||
deleteFn: (id: number) => Promise<any>,
|
||||
store: any,
|
||||
itemName: string,
|
||||
) {
|
||||
openConfirmation({
|
||||
title: `Подтвердите удаление`,
|
||||
body: `Вы уверены, что хотите удалить ${itemName} "${item.name}"?`,
|
||||
confirmText: "Удалить",
|
||||
confirmVariant: "danger",
|
||||
onConfirm: () => {
|
||||
// deleteFn(item.id).then(() => { // TODO: Uncomment when API is ready
|
||||
store.update((items: T[]) => items.filter((i) => i.id !== item.id));
|
||||
addToast({
|
||||
header: `${itemName} удален`,
|
||||
body: `${itemName} "${item.name}" успешно удален.`,
|
||||
color: "success",
|
||||
});
|
||||
// }).catch(error => addToast({ header: 'Ошибка', body: `Не удалось удалить ${itemName}: ${error.message}`, color: 'danger' }));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditPoint(point: SavedPoint) {
|
||||
editPoint = point;
|
||||
}
|
||||
async function confirmDelete() {
|
||||
if (!toDelete) return;
|
||||
try {
|
||||
await pointsApi.delete(toDelete.id);
|
||||
pointsStore.update((items) => items.filter((p) => p.id !== toDelete!.id));
|
||||
addToast({ header: $t('common.success'), body: toDelete.name, color: 'success' });
|
||||
} catch (err: unknown) {
|
||||
addToast({ header: $t('common.error'), body: (err as Error).message, color: 'danger' });
|
||||
} finally {
|
||||
toDelete = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="force-page-height">
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<!-- Spacer for fixed navbar -->
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<!-- Side Navigation -->
|
||||
<div class="col-md-3 col-lg-2 mb-4">
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link" href="/user/account">Учетная запись</a>
|
||||
<a class="nav-link active" href="/user/templates">Сохраненные сценарии</a>
|
||||
<a class="nav-link" href="#/">История прогнозов</a>
|
||||
<a class="nav-link" href="#/">История слежения</a>
|
||||
</nav>
|
||||
</div>
|
||||
<Navbar />
|
||||
<div style="height: var(--navbar-height);"></div>
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-2 mb-4">
|
||||
<nav class="nav nav-pills flex-column">
|
||||
<a class="nav-link" href="/user/account">{$t('nav.account')}</a>
|
||||
<a class="nav-link active" href="/user/templates">{$t('nav.scenarios')}</a>
|
||||
<a class="nav-link" href="/user/predictions">{$t('nav.predictionHistory')}</a>
|
||||
<a class="nav-link" href="/user/flights">{$t('nav.trackingHistory')}</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<!-- Saved Points -->
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Точки запуска</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div class="position-relative mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm pe-5"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={pointsSearch.value}
|
||||
oninput={() => pointsSearch.set()}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y me-2 rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white);"
|
||||
onclick={() => {
|
||||
pointsSearch.value = "";
|
||||
pointsSearch.set();
|
||||
}}
|
||||
disabled={!pointsSearch.value}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Широта</th>
|
||||
<th>Долгота</th>
|
||||
<th>Высота</th>
|
||||
<th class="fit"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each pointsTable.rows as row}
|
||||
<tr>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.lat.toFixed(4)} °</td>
|
||||
<td>{row.lon.toFixed(4)} °</td>
|
||||
<td>{row.alt} м</td>
|
||||
<td class="fit">
|
||||
<Button color="primary" size="sm" onclick={() => handleEditPoint(row)}>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
handleDelete(row, deletePoint, SavedPointsStore, "Точка")}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination aria-label="Points page navigation" size="sm">
|
||||
<PaginationItem>
|
||||
<PaginationLink previous onclick={() => pointsTable.setPage("previous")} />
|
||||
</PaginationItem>
|
||||
{#each pointsTable.pagesWithEllipsis as page}
|
||||
<PaginationItem active={pointsTable.currentPage === page}>
|
||||
<PaginationLink onclick={() => pointsTable.setPage(page)}>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
{/each}
|
||||
<PaginationItem>
|
||||
<PaginationLink next onclick={() => pointsTable.setPage("next")} />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<!-- Saved Flight Profiles -->
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Профили полета</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm mb-2"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={profilesSearch.value}
|
||||
oninput={() => profilesSearch.set()}
|
||||
/>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Скороподъемность</th>
|
||||
<th>Скорость снижения</th>
|
||||
<th>Высота разрыва</th>
|
||||
<th class="fit"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each profilesTable.rows as row}
|
||||
<tr>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.rate_profile_data.ascent_rate} м/с</td>
|
||||
<td>{row.rate_profile_data.descent_rate} м/с</td>
|
||||
<td>{row.rate_profile_data.burst_altitude} м</td>
|
||||
<td class="fit">
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
handleDelete(
|
||||
row,
|
||||
deleteFlightProfile,
|
||||
SavedFlightProfilesStore,
|
||||
"Профиль",
|
||||
)}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination aria-label="Profiles page navigation" size="sm">
|
||||
<PaginationItem>
|
||||
<PaginationLink previous onclick={() => profilesTable.setPage("previous")} />
|
||||
</PaginationItem>
|
||||
{#each profilesTable.pagesWithEllipsis as page}
|
||||
<PaginationItem active={profilesTable.currentPage === page}>
|
||||
<PaginationLink onclick={() => profilesTable.setPage(page)}>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
{/each}
|
||||
<PaginationItem>
|
||||
<PaginationLink next onclick={() => profilesTable.setPage("next")} />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<!-- Saved Scenario Templates -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Сценарии</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm mb-2"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={templatesSearch.value}
|
||||
oninput={() => templatesSearch.set()}
|
||||
/>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th class="fit"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each templatesTable.rows as row}
|
||||
<tr>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.template_data.description}</td>
|
||||
<td class="fit">
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
handleDelete(
|
||||
row,
|
||||
deleteScenarioTemplate,
|
||||
SavedScenarioTemplatesStore,
|
||||
"Шаблон",
|
||||
)}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination aria-label="Templates page navigation" size="sm">
|
||||
<PaginationItem>
|
||||
<PaginationLink previous onclick={() => templatesTable.setPage("previous")} />
|
||||
</PaginationItem>
|
||||
{#each templatesTable.pagesWithEllipsis as page}
|
||||
<PaginationItem active={templatesTable.currentPage === page}>
|
||||
<PaginationLink onclick={() => templatesTable.setPage(page)}>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
{/each}
|
||||
<PaginationItem>
|
||||
<PaginationLink next onclick={() => templatesTable.setPage("next")} />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<Card class="mb-4">
|
||||
<CardHeader><h5 class="mb-0">{$t('points.items')}</h5></CardHeader>
|
||||
<CardBody>
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm mb-2"
|
||||
placeholder={$t('editor.searchPlaceholder')}
|
||||
bind:value={search.value}
|
||||
oninput={() => search.set()} />
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t('points.name')}</th>
|
||||
<th>{$t('points.lat')}</th>
|
||||
<th>{$t('points.lon')}</th>
|
||||
<th>{$t('points.alt')}</th>
|
||||
<th class="fit"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each table.rows as row}
|
||||
<tr>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.lat.toFixed(4)} °</td>
|
||||
<td>{row.lon.toFixed(4)} °</td>
|
||||
<td>{row.alt} м</td>
|
||||
<td class="fit">
|
||||
<Button color="primary" size="sm" onclick={() => (editPoint = row)}>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
<Button color="danger" size="sm" onclick={() => (toDelete = row)}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination size="sm">
|
||||
<PaginationItem>
|
||||
<PaginationLink previous onclick={() => table.setPage('previous')} />
|
||||
</PaginationItem>
|
||||
{#each table.pagesWithEllipsis as page}
|
||||
<PaginationItem active={table.currentPage === page}>
|
||||
<PaginationLink onclick={() => table.setPage(page)}>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
{/each}
|
||||
<PaginationItem>
|
||||
<PaginationLink next onclick={() => table.setPage('next')} />
|
||||
</PaginationItem>
|
||||
</Pagination>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
<ConfirmationPrompt
|
||||
bind:isOpen={showConfirm}
|
||||
title={confirmConfig.title}
|
||||
confirmText={confirmConfig.confirmText}
|
||||
confirmVariant={confirmConfig.confirmVariant || "danger"}
|
||||
cancelText="Отмена"
|
||||
onconfirm={handleConfirm}
|
||||
oncancel={() => (showConfirm = false)}
|
||||
>
|
||||
<p>{confirmConfig.body}</p>
|
||||
isOpen={toDelete !== null}
|
||||
title={$t('editor.delete')}
|
||||
confirmText={$t('editor.delete')}
|
||||
cancelText={$t('editor.cancel')}
|
||||
confirmVariant="danger"
|
||||
onconfirm={confirmDelete}
|
||||
oncancel={() => (toDelete = null)}>
|
||||
{#if toDelete}
|
||||
<p>Delete "{toDelete.name}"?</p>
|
||||
{/if}
|
||||
</ConfirmationPrompt>
|
||||
|
||||
<PointEditor
|
||||
point={editPoint}
|
||||
isOpen={editPoint !== null}
|
||||
onClose={() => { editPoint = null; pointsTable.setRows($SavedPointsStore) }}
|
||||
editor={true}
|
||||
closeOnSave={true}
|
||||
closeOnDelete={true}
|
||||
/>
|
||||
|
||||
<ToastContainer />
|
||||
point={editPoint}
|
||||
isOpen={editPoint !== null}
|
||||
onClose={() => (editPoint = null)} />
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ssr = false;
|
||||
Loading…
Add table
Add a link
Reference in a new issue