Add profile and template pages (scaffolding)
This commit is contained in:
parent
41668498ea
commit
cb67c5d93d
18 changed files with 1067 additions and 158 deletions
19
src/lib/api/profiles.ts
Normal file
19
src/lib/api/profiles.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* API functions for SavedFlightProfile */
|
||||
import type {SavedFlightProfile } from "$lib/types";
|
||||
import { getAPI, postAPI, putAPI, deleteAPI } from "./base";
|
||||
|
||||
export function getSavedFlightProfiles(): Promise<SavedFlightProfile[]> {
|
||||
return getAPI<SavedFlightProfile[]>("/saved-profiles/");
|
||||
}
|
||||
|
||||
export function saveFlightProfile(profile: SavedFlightProfile): Promise<SavedFlightProfile> {
|
||||
return postAPI<SavedFlightProfile>("/saved-profiles/", profile);
|
||||
}
|
||||
|
||||
export function updateFlightProfile(profile: SavedFlightProfile): Promise<SavedFlightProfile> {
|
||||
return putAPI<SavedFlightProfile>(`/saved-profiles/${profile.id}/`, profile);
|
||||
}
|
||||
|
||||
export function deleteFlightProfile(id: number): Promise<void> {
|
||||
return deleteAPI<void>(`/saved-profiles/${id}/`);
|
||||
}
|
||||
19
src/lib/api/templates.ts
Normal file
19
src/lib/api/templates.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* API functions for SavedScenarioTemplate */
|
||||
import type { SavedScenarioTemplate } from "$lib/types";
|
||||
import { getAPI, postAPI, putAPI, deleteAPI } from "./base";
|
||||
|
||||
export function getSavedScenarioTemplates(): Promise<SavedScenarioTemplate[]> {
|
||||
return getAPI<SavedScenarioTemplate[]>("/saved-templates/");
|
||||
}
|
||||
|
||||
export function saveScenarioTemplate(template: SavedScenarioTemplate): Promise<SavedScenarioTemplate> {
|
||||
return postAPI<SavedScenarioTemplate>("/saved-templates/", template);
|
||||
}
|
||||
|
||||
export function updateScenarioTemplate(template: SavedScenarioTemplate): Promise<SavedScenarioTemplate> {
|
||||
return putAPI<SavedScenarioTemplate>(`/saved-templates/${template.id}/`, template);
|
||||
}
|
||||
|
||||
export function deleteScenarioTemplate(id: number): Promise<void> {
|
||||
return deleteAPI<void>(`/saved-templates/${id}/`);
|
||||
}
|
||||
159
src/lib/auth.ts
159
src/lib/auth.ts
|
|
@ -5,76 +5,129 @@ export const LOGIN_URL = 'http://localhost:8000/api/login/';
|
|||
export const LOGOUT_URL = 'http://localhost:8000/api/logout/';
|
||||
export const SESSION_URL = 'http://localhost:8000/api/session/';
|
||||
export const WHOAMI_URL = 'http://localhost:8000/api/whoami/';
|
||||
|
||||
export async function getCsrfToken(): Promise<string | null> {
|
||||
return Cookies.get('csrftoken') || null;
|
||||
}
|
||||
|
||||
export async function getCsrfTokenAuth(): Promise<string | null> {
|
||||
const response = await fetch(CSRF_URL, {});
|
||||
console.log('CSRF Token Response:', response);
|
||||
return Cookies.get('csrftoken') || null;
|
||||
try {
|
||||
await fetch(CSRF_URL, {});
|
||||
return Cookies.get('csrftoken') || null;
|
||||
} catch (error) {
|
||||
console.error('Failed to get CSRF token:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAuthenticated(): Promise<boolean> {
|
||||
const csrfToken = await getCsrfTokenAuth();
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found');
|
||||
try {
|
||||
const csrfToken = await getCsrfTokenAuth();
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found');
|
||||
}
|
||||
const response = await fetch(SESSION_URL, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Authentication check failed: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.isAuthenticated;
|
||||
} catch (error) {
|
||||
console.error('Authentication check failed:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
const response = await fetch(SESSION_URL, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
});
|
||||
let data = await (response as Response).json();
|
||||
return data.isAuthenticated;
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string): Promise<void> {
|
||||
const csrfToken = await getCsrfTokenAuth();
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found');
|
||||
export async function login(username: string, password: string): Promise<any> {
|
||||
try {
|
||||
const csrfToken = await getCsrfTokenAuth();
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found');
|
||||
}
|
||||
|
||||
const response = await fetch(LOGIN_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(`Login failed: ${response.statusText} - ${errorData.detail || ''}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const response = await fetch(LOGIN_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Login failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
const csrfToken = await getCsrfTokenAuth();
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found');
|
||||
try {
|
||||
const csrfToken = await getCsrfTokenAuth();
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found');
|
||||
}
|
||||
|
||||
const response = await fetch(LOGOUT_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Logout failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
console.log('Logout successful');
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(LOGOUT_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
export async function whoami(): Promise<any> {
|
||||
try {
|
||||
const csrfToken = await getCsrfTokenAuth();
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token not found');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Logout failed: ${response.statusText}`);
|
||||
const response = await fetch(WHOAMI_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Whoami failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !data.username) {
|
||||
throw new Error('No user data found');
|
||||
}
|
||||
|
||||
return data.username;
|
||||
} catch (error) {
|
||||
console.error('Whoami failed:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
console.log('Logout successful');
|
||||
return;
|
||||
}
|
||||
44
src/lib/components/ConfirmationPrompt.svelte
Normal file
44
src/lib/components/ConfirmationPrompt.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script>
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from "@sveltestrap/sveltestrap";
|
||||
|
||||
let {
|
||||
isOpen = $bindable(false),
|
||||
title = 'Confirm Action',
|
||||
confirmText = 'Confirm',
|
||||
cancelText = 'Cancel',
|
||||
confirmVariant = 'primary',
|
||||
cancelVariant = 'secondary',
|
||||
onconfirm,
|
||||
oncancel,
|
||||
children
|
||||
} = $props();
|
||||
|
||||
function handleConfirm() {
|
||||
onconfirm?.();
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
oncancel?.();
|
||||
isOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal {isOpen} toggle={handleCancel} fade={false} backdrop={true}>
|
||||
<ModalHeader toggle={handleCancel}>{title}</ModalHeader>
|
||||
<ModalBody>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
Вы действительно хотите продолжить?
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color={cancelVariant} on:click={handleCancel}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button color={confirmVariant} on:click={handleConfirm}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
36
src/lib/components/Footer.svelte
Normal file
36
src/lib/components/Footer.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!-- Footer -->
|
||||
<footer class="bg-dark text-bg-dark mt-auto">
|
||||
<div class="container pt-5">
|
||||
<div class="row gy-5">
|
||||
<div class="col-lg-3 mw-lg-2">
|
||||
<div class="mb-4">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img
|
||||
src="/logo-full-ru-dark.svg"
|
||||
class="d-inline-block align-middle img-fluid"
|
||||
alt="ООО «ЯКС»"
|
||||
width="250"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8 offset-lg-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container pb-4">
|
||||
<div class="row">
|
||||
<div class="col-6 small">
|
||||
<div>Copyright © 2024 ООО «Якутские Космические Системы»</div>
|
||||
</div>
|
||||
<div class="col-6 text-end small">
|
||||
<div>
|
||||
<p>
|
||||
<a class="text-decoration-none" href="/usage_policy">Условия использования</a> -
|
||||
<a class="text-decoration-none" href="/privacy">Политика конфиденциальности</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { checkAuthenticated, logout } from '$lib/auth';
|
||||
import { checkAuthenticated, logout, whoami } from '$lib/auth';
|
||||
import {
|
||||
Collapse,
|
||||
Dropdown,
|
||||
|
|
@ -19,30 +20,35 @@
|
|||
// State for the navbar toggler
|
||||
let isOpen = false;
|
||||
|
||||
// Check if user is authenticated (using localStorage token)
|
||||
let isAuthenticated = false;
|
||||
// Authentication state
|
||||
let isAuthenticated: boolean | null = null; // null represents the initial, unknown state
|
||||
let user: string | null = null;
|
||||
|
||||
// This should be reactive to changes in auth status
|
||||
$: if (typeof window !== 'undefined') {
|
||||
Promise.resolve(checkAuthenticated()).then((result) => {
|
||||
isAuthenticated = result;
|
||||
});
|
||||
} else {
|
||||
isAuthenticated = false;
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
const authStatus = await checkAuthenticated();
|
||||
isAuthenticated = authStatus;
|
||||
if (authStatus) {
|
||||
user = await whoami();
|
||||
} else {
|
||||
user = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication check failed:', error);
|
||||
isAuthenticated = false;
|
||||
user = null;
|
||||
}
|
||||
});
|
||||
|
||||
function handleLogout() {
|
||||
// Clear authentication tokens
|
||||
try {
|
||||
logout();
|
||||
isAuthenticated = false;
|
||||
user = null;
|
||||
goto('/');
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
// Update auth status
|
||||
isAuthenticated = false;
|
||||
|
||||
// Redirect to login page
|
||||
goto('/');
|
||||
}
|
||||
</script>
|
||||
<Navbar color="light" light expand="lg" fixed="top" class="custom-navbar border-bottom">
|
||||
|
|
@ -70,30 +76,31 @@
|
|||
</NavItem>
|
||||
</Nav>
|
||||
<Nav navbar>
|
||||
{#if isAuthenticated}
|
||||
{#if isAuthenticated === true && user}
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret class="nav-full-height border border-top-0">
|
||||
Account
|
||||
{user ?? 'Пользователь'}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu end>
|
||||
<DropdownItem href="/user/account">Account Settings</DropdownItem>
|
||||
<DropdownItem href="/user/templates">Saved Templates</DropdownItem>
|
||||
<DropdownItem href="/user/predictions">Prediction History</DropdownItem>
|
||||
<DropdownItem href="/user/flights">Flight History</DropdownItem>
|
||||
<DropdownItem href="/user/account">Учетная запись</DropdownItem>
|
||||
<DropdownItem href="/user/templates">Сохраненные сценарии</DropdownItem>
|
||||
<DropdownItem href="/user/predictions">История прогнозов</DropdownItem>
|
||||
<DropdownItem href="/user/flights">История слежения</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
<DropdownItem on:click={handleLogout}>Logout</DropdownItem>
|
||||
<DropdownItem on:click={handleLogout}>Выйти</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{:else}
|
||||
{:else if isAuthenticated === false}
|
||||
<NavItem>
|
||||
<NavLink
|
||||
href="/login"
|
||||
class="nav-full-height border border-top-0"
|
||||
active={$page.url.pathname === '/login'}>
|
||||
Login
|
||||
Войти
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{/if}
|
||||
<!-- While isAuthenticated is null (loading), nothing is rendered in this block -->
|
||||
</Nav>
|
||||
</div>
|
||||
</Navbar>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { TableHandler } from '@vincjo/datatables';
|
||||
import { Modal,
|
||||
import { TableHandler } from "@vincjo/datatables";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
FormGroup,
|
||||
Label,
|
||||
|
|
@ -10,26 +11,26 @@
|
|||
Pagination,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
import { onMount } from 'svelte';
|
||||
import { addToast } from '$lib/components/Toast.svelte';
|
||||
import type { SavedPoint } from '$lib/types';
|
||||
import { SavedPointsStore } from '$lib/stores';
|
||||
import { getSavedPoints, savePoint, updatePoint, deletePoint } from '$lib/api/points';
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
import type { SavedPoint } from "$lib/types";
|
||||
import { SavedPointsStore } from "$lib/stores";
|
||||
import { getSavedPoints, savePoint, updatePoint, deletePoint } from "$lib/api/points";
|
||||
|
||||
// Props
|
||||
let { isOpen = $bindable(false), onClose = () => {}, onChange = () => {} } = $props();
|
||||
|
||||
// Runes
|
||||
let selectedPoint = $state<SavedPoint | null>(null);
|
||||
let newPoint = $state<SavedPoint>({ id: 0, name: '', lat: 0, lon: 0, alt: 0 });
|
||||
let newPoint = $state<SavedPoint>({ id: 0, name: "", lat: 0, lon: 0, alt: 0 });
|
||||
let isEditing = $state(false);
|
||||
let isAlertVisible = $state(false);
|
||||
let alertText = $state('');
|
||||
let alertText = $state("");
|
||||
|
||||
// Table handler
|
||||
let table = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 10 }));
|
||||
let search = $derived(table.createSearch(['name']));
|
||||
let search = $derived(table.createSearch(["name"]));
|
||||
|
||||
$effect(() => {
|
||||
onChange();
|
||||
|
|
@ -59,48 +60,54 @@
|
|||
}
|
||||
|
||||
function handleDeletePoint(point: SavedPoint) {
|
||||
deletePoint(point.id).then(() => {
|
||||
$SavedPointsStore = $SavedPointsStore.filter(p => p.id !== point.id);
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
addToast({
|
||||
header: 'Точка удалена',
|
||||
body: `Точка "${point.name}" успешно удалена.`,
|
||||
color: 'success',
|
||||
deletePoint(point.id)
|
||||
.then(() => {
|
||||
$SavedPointsStore = $SavedPointsStore.filter((p) => p.id !== point.id);
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
addToast({
|
||||
header: "Точка удалена",
|
||||
body: `Точка "${point.name}" успешно удалена.`,
|
||||
color: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при удалении точки: ${error.message}`);
|
||||
console.error("Ошибка при удалении точки:", error);
|
||||
});
|
||||
}).catch(error => {
|
||||
showAlert(`Ошибка при удалении точки: ${error.message}`);
|
||||
console.error('Ошибка при удалении точки:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function handleSavePoint() {
|
||||
if (isEditing && selectedPoint) {
|
||||
updatePoint(newPoint).then(updatedPoint => {
|
||||
$SavedPointsStore = $SavedPointsStore.map(p => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: 'Точка обновлена',
|
||||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: 'success',
|
||||
updatePoint(newPoint)
|
||||
.then((updatedPoint) => {
|
||||
$SavedPointsStore = $SavedPointsStore.map((p) => (p.id === updatedPoint.id ? updatedPoint : p));
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Точка обновлена",
|
||||
body: `Точка "${updatedPoint.name}" успешно обновлена.`,
|
||||
color: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при обновлении точки: ${error.message}`);
|
||||
});
|
||||
}).catch(error => {
|
||||
showAlert(`Ошибка при обновлении точки: ${error.message}`);
|
||||
});
|
||||
} else {
|
||||
savePoint(newPoint).then(savedPoint => {
|
||||
$SavedPointsStore = [...$SavedPointsStore, savedPoint];
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: 'Точка сохранена',
|
||||
body: `Точка "${savedPoint.name}" успешно сохранена.`,
|
||||
color: 'success',
|
||||
savePoint(newPoint)
|
||||
.then((savedPoint) => {
|
||||
$SavedPointsStore = [...$SavedPointsStore, savedPoint];
|
||||
SavedPointsStore.set($SavedPointsStore);
|
||||
resetForm();
|
||||
addToast({
|
||||
header: "Точка сохранена",
|
||||
body: `Точка "${savedPoint.name}" успешно сохранена.`,
|
||||
color: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
showAlert(`Ошибка при сохранении точки: ${error.message}`);
|
||||
console.error("Ошибка при сохранении точки:", error);
|
||||
});
|
||||
}).catch(error => {
|
||||
showAlert(`Ошибка при сохранении точки: ${error.message}`);
|
||||
console.error('Ошибка при сохранении точки:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,12 +118,12 @@
|
|||
|
||||
export function hideAlert() {
|
||||
isAlertVisible = false;
|
||||
alertText = '';
|
||||
alertText = "";
|
||||
}
|
||||
|
||||
export function resetForm() {
|
||||
selectedPoint = null;
|
||||
newPoint = { id: 0, name: '', lat: 0, lon: 0, alt: 0 };
|
||||
newPoint = { id: 0, name: "", lat: 0, lon: 0, alt: 0 };
|
||||
isEditing = false;
|
||||
hideAlert();
|
||||
}
|
||||
|
|
@ -130,21 +137,24 @@
|
|||
<div class="modal-body">
|
||||
<div class="position-relative mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm pe-5"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={search.value}
|
||||
oninput={() => search.set()}
|
||||
type="text"
|
||||
class="form-control-sm pe-5"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={search.value}
|
||||
oninput={() => search.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={() => { search.value = ''; search.set(); }}
|
||||
disabled={!search.value}
|
||||
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={() => {
|
||||
search.value = "";
|
||||
search.set();
|
||||
}}
|
||||
disabled={!search.value}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<div bind:this={table.element} class="table-responsive">
|
||||
|
|
@ -196,13 +206,23 @@
|
|||
|
||||
<!-- Form for adding/editing points -->
|
||||
<div>
|
||||
<h5>{isEditing ? 'Редактирование точки' : 'Добавить новую точку'}</h5>
|
||||
<Alert color="danger" isOpen={isAlertVisible} toggle={() => (isAlertVisible = false)} fade={false}
|
||||
class="mb-2">
|
||||
<h5>{isEditing ? "Редактирование точки" : "Добавить новую точку"}</h5>
|
||||
<Alert
|
||||
color="danger"
|
||||
isOpen={isAlertVisible}
|
||||
toggle={() => (isAlertVisible = false)}
|
||||
fade={false}
|
||||
class="mb-2"
|
||||
>
|
||||
<Icon name="exclamation-triangle" class="me-2" />
|
||||
{alertText}
|
||||
</Alert>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSavePoint(); }}>
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSavePoint();
|
||||
}}
|
||||
>
|
||||
<div class="mb-2">
|
||||
<Label for="name" class="small">Название точки:</Label>
|
||||
<Input class="form-control-sm" type="text" id="name" bind:value={newPoint.name} required />
|
||||
|
|
@ -210,22 +230,43 @@
|
|||
<div class="d-flex gap-2">
|
||||
<FormGroup class="flex-grow-1">
|
||||
<Label for="lat" class="small">Широта:</Label>
|
||||
<Input class="form-control-sm" type="number" step="any" id="lat" bind:value={newPoint.lat} required />
|
||||
<Input
|
||||
class="form-control-sm"
|
||||
type="number"
|
||||
step="any"
|
||||
id="lat"
|
||||
bind:value={newPoint.lat}
|
||||
required
|
||||
/>
|
||||
<span class="form-text">Градусы</span>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-grow-1">
|
||||
<Label for="lon" class="small">Долгота:</Label>
|
||||
<Input class="form-control-sm" type="number" step="any" id="lon" bind:value={newPoint.lon} required />
|
||||
<Input
|
||||
class="form-control-sm"
|
||||
type="number"
|
||||
step="any"
|
||||
id="lon"
|
||||
bind:value={newPoint.lon}
|
||||
required
|
||||
/>
|
||||
<span class="form-text">Градусы</span>
|
||||
</FormGroup>
|
||||
<FormGroup class="flex-grow-1">
|
||||
<Label for="alt" class="small">Высота:</Label>
|
||||
<Input class="form-control-sm" type="number" step="any" id="alt" bind:value={newPoint.alt} required />
|
||||
<Input
|
||||
class="form-control-sm"
|
||||
type="number"
|
||||
step="any"
|
||||
id="alt"
|
||||
bind:value={newPoint.alt}
|
||||
required
|
||||
/>
|
||||
<span class="form-text">Метры над ур. моря</span>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<Button type="submit" color="success" size="sm">
|
||||
{isEditing ? 'Обновить точку' : 'Сохранить точку'}
|
||||
{isEditing ? "Обновить точку" : "Сохранить точку"}
|
||||
</Button>
|
||||
{#if isEditing}
|
||||
<Button size="sm" type="button" color="secondary" onclick={resetForm}>Отмена</Button>
|
||||
|
|
|
|||
|
|
@ -75,12 +75,14 @@
|
|||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
class="mb-2 w-100"
|
||||
class="mb-0 w-100"
|
||||
>
|
||||
Редактировать сохраненные сценарии
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
</Button>
|
||||
|
||||
<hr />
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Режим сценария:</Label>
|
||||
<InputGroup size="sm">
|
||||
|
|
@ -92,8 +94,32 @@
|
|||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Модель атмосферы:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode">
|
||||
<option>GFS (0.25°)</option>
|
||||
<option>GFS (0.5°)</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Набор данных:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode">
|
||||
<option>Выбрать автоматически</option>
|
||||
<!-- TODO ручка апи для доступных наборов -->
|
||||
<option>20250701-00</option>
|
||||
<option>20250701-06</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<hr />
|
||||
|
||||
<FormGroup spacing="mb-0">
|
||||
<Label for="export" class="form-label">Экспортировать:</Label>
|
||||
<Label for="export" class="form-label">Экспортировать результат:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="export">
|
||||
<option>JSON</option>
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@
|
|||
<style>
|
||||
.select-container {
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
import type { FlightParameters, RawTelemetry, Telemetry } from "./types";
|
||||
import type { RawPrediction, Prediction } from "./types";
|
||||
import type { SavedPoint } from "./types";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate } from "./types";
|
||||
|
||||
export const readLocalStorage = <T>(key: string, defaultValue: T): T => {
|
||||
const item = localStorage.getItem(key);
|
||||
|
|
@ -70,3 +70,9 @@ export const PredictionStore = writable<Prediction>(
|
|||
);
|
||||
|
||||
export const SavedPointsStore = writable<SavedPoint[]>([]);
|
||||
|
||||
// stub
|
||||
export const SavedFlightProfilesStore = writable<SavedFlightProfile[]>([]);
|
||||
|
||||
// stub
|
||||
export const SavedScenarioTemplatesStore = writable<SavedScenarioTemplate[]>([]);
|
||||
|
|
@ -101,4 +101,16 @@ export interface SavedPoint {
|
|||
lat: number;
|
||||
lon: number;
|
||||
alt: number;
|
||||
}
|
||||
|
||||
export interface SavedFlightProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
rate_profile_data: object;
|
||||
}
|
||||
|
||||
export interface SavedScenarioTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
template_data: object;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue