Add profile and template pages (scaffolding)
This commit is contained in:
parent
41668498ea
commit
cb67c5d93d
18 changed files with 1067 additions and 158 deletions
345
src/routes/user/templates/+page.svelte
Normal file
345
src/routes/user/templates/+page.svelte
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
<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 Navbar from "$lib/components/Navbar.svelte";
|
||||
import Footer from "$lib/components/Footer.svelte";
|
||||
import ConfirmationPrompt from "$lib/components/ConfirmationPrompt.svelte";
|
||||
import { addToast } from "$lib/components/Toast.svelte";
|
||||
|
||||
// TODO: Implement these imports
|
||||
import { SavedPointsStore, SavedFlightProfilesStore, SavedScenarioTemplatesStore } from "$lib/stores";
|
||||
import { getSavedPoints, deletePoint } from "$lib/api/points";
|
||||
import { getSavedFlightProfiles, deleteFlightProfile } from "$lib/api/profiles";
|
||||
import { getSavedScenarioTemplates, deleteScenarioTemplate } from "$lib/api/templates";
|
||||
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate } from "$lib/types";
|
||||
|
||||
// Table handlers
|
||||
let pointsTable = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 5 }));
|
||||
let pointsSearch = $derived(pointsTable.createSearch(["name"]));
|
||||
|
||||
let profilesTable = $derived(new TableHandler($SavedFlightProfilesStore, { rowsPerPage: 5 }));
|
||||
let profilesSearch = $derived(profilesTable.createSearch(["name"]));
|
||||
|
||||
let templatesTable = $derived(new TableHandler($SavedScenarioTemplatesStore, { rowsPerPage: 5 }));
|
||||
let templatesSearch = $derived(templatesTable.createSearch(["name"]));
|
||||
|
||||
onMount(async () => {
|
||||
// Mock data for demonstration. Replace with API calls.
|
||||
$SavedPointsStore = [
|
||||
{ id: 1, name: "Baikonur Cosmodrome", lat: 45.96, lon: 63.3, alt: 90 },
|
||||
{ id: 2, name: "Kennedy Space Center", lat: 28.57, lon: -80.64, alt: 3 },
|
||||
];
|
||||
$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} },
|
||||
];
|
||||
$SavedScenarioTemplatesStore = [
|
||||
{ id: 1, name: "Summer Launch from Baikonur", template_data: {description: "Standard summer conditions test."} },
|
||||
{ id: 2, name: "Winter Launch from KSC", template_data: {description: "High wind scenario."} },
|
||||
];
|
||||
|
||||
/*
|
||||
// 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' }));
|
||||
},
|
||||
});
|
||||
}
|
||||
</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">
|
||||
<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>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<!-- Saved Points -->
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<h5 class="mb-0">Точки запуска</h5>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Input
|
||||
type="text"
|
||||
class="form-control-sm mb-2"
|
||||
placeholder="Поиск по названию..."
|
||||
bind:value={pointsSearch.value}
|
||||
oninput={() => pointsSearch.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 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="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 />
|
||||
</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>
|
||||
</ConfirmationPrompt>
|
||||
Loading…
Add table
Add a link
Reference in a new issue