Add profile and template pages (scaffolding)

This commit is contained in:
ThePetrovich 2025-07-03 18:39:04 +08:00
parent 41668498ea
commit cb67c5d93d
18 changed files with 1067 additions and 158 deletions

View file

@ -80,6 +80,7 @@
<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} >
<TabComponent

View file

@ -12,6 +12,7 @@
<main>
<Navbar />
<div style="height: var(--navbar-height);"></div> <!-- Spacer for fixed navbar -->
<Map>
<TelemetryPanel
/>

View file

@ -1,12 +1,267 @@
<script lang="ts">
import Navbar from '$lib/components/Navbar.svelte';
import Navbar from "$lib/components/Navbar.svelte";
import {
Card,
CardHeader,
CardBody,
Button,
FormGroup,
Label,
Input,
InputGroup,
InputGroupText,
Icon,
} from "@sveltestrap/sveltestrap";
import ConfirmationPrompt from "$lib/components/ConfirmationPrompt.svelte";
import Footer from "$lib/components/Footer.svelte";
let editMode = false;
let showToken = false;
type ConfirmConfig = {
title: string;
body: string;
confirmText: string;
confirmVariant?: string;
onConfirm: () => void;
};
// 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;
}
</script>
<main>
<main class="force-page-height">
<Navbar />
<div class="container">
<h1>User Account</h1>
<p>Manage your account settings here.</p>
<!-- Add account management components or links here -->
<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 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>
<!-- 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>
</main>
<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>
</ConfirmationPrompt>

View 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>