Skip to content

Commit

Permalink
feat: add settings page for managing airports
Browse files Browse the repository at this point in the history
  • Loading branch information
johanohly committed Jan 7, 2025
1 parent 47add4d commit ad0d65a
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
-- CreateTable
CREATE TABLE "airport" (
"code" TEXT NOT NULL,
"iata" TEXT,
Expand All @@ -8,6 +9,7 @@ CREATE TABLE "airport" (
"type" TEXT NOT NULL,
"continent" TEXT NOT NULL,
"country" TEXT NOT NULL,
"custom" BOOLEAN NOT NULL DEFAULT false,

CONSTRAINT "airport_pkey" PRIMARY KEY ("code")
);
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ model airport {
/// @kyselyType('EU' | 'NA' | 'SA' | 'AS' | 'AF' | 'OC' | 'AN')
continent String
country String
custom Boolean @default(false)
}

model flight {
Expand Down
8 changes: 6 additions & 2 deletions src/lib/components/modals/settings/SettingsModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
ExportPage,
OAuthPage,
SecurityPage,
DataPage,
} from './pages';
import { page } from '$app/stores';
import { page } from '$app/state';
import SettingsTabContainer from '$lib/components/modals/settings/SettingsTabContainer.svelte';
import { Button } from '$lib/components/ui/button';
import { Modal } from '$lib/components/ui/modal';
Expand All @@ -28,6 +29,7 @@
{ title: 'Export', id: 'export' },
] as const;
const ADMIN_SETTINGS = [
{ title: 'Data', id: 'data' },
{ title: 'Users', id: 'users' },
{ title: 'OAuth', id: 'oauth' },
] as const;
Expand All @@ -48,7 +50,7 @@
}
});
const user = $derived($page.data.user);
const user = $derived(page.data.user);
const [send, receive] = crossfade({
duration: 250,
Expand Down Expand Up @@ -141,6 +143,8 @@
<ImportPage bind:open />
{:else if activeTab === 'export'}
<ExportPage />
{:else if activeTab === 'data'}
<DataPage />
{:else if activeTab === 'users'}
<UsersPage />
{:else if activeTab === 'oauth'}
Expand Down
19 changes: 19 additions & 0 deletions src/lib/components/modals/settings/pages/data-page/DataPage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { PageHeader } from '../';
import { Collapsible } from '$lib/components/ui/collapsible';
</script>

<PageHeader
title="Data"
subtitle="Manage custom airports, airlines and airplanes."
>
<div class="flex">
<Collapsible
title="Airports"
subtitle="Add airports not found in the official list."
>
Hello World!
</Collapsible>
</div>
</PageHeader>
1 change: 1 addition & 0 deletions src/lib/components/modals/settings/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { default as SecurityPage } from './security-page/SecurityPage.svelte';
export { default as ImportPage } from './import-page/ImportPage.svelte';
export { default as ExportPage } from './ExportPage.svelte';
export { default as OAuthPage } from './OAuthPage.svelte';
export { default as DataPage } from './data-page/DataPage.svelte';
104 changes: 41 additions & 63 deletions src/lib/components/modals/settings/pages/security-page/ApiKeys.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
<script lang="ts">
import { ChevronRight, X } from '@o7/icon/lucide';
import { Collapsible } from 'bits-ui';
import { X } from '@o7/icon/lucide';
import { formatRelative } from 'date-fns';
import { slide } from 'svelte/transition';
import { Confirm } from '$lib/components/helpers';
import CreateKey from '$lib/components/modals/settings/pages/security-page/CreateKey.svelte';
import { Button } from '$lib/components/ui/button';
import { Card } from '$lib/components/ui/card';
import { Collapsible } from '$lib/components/ui/collapsible';
import type { ApiKey } from '$lib/db/types';
import { api } from '$lib/trpc';
import { cn } from '$lib/utils';
let open = $state(false);
let loaded = $state(false);
let keys: ApiKey[] = $state([]);
Expand All @@ -27,62 +23,44 @@
});
</script>

<Collapsible.Root
bind:open
<Collapsible
title="API Keys"
subtitle="Manage your API keys"
disabled={!loaded}
class={cn('flex flex-col p-4 rounded-lg border', { 'opacity-80': !loaded })}
class={{ 'opacity-80': !loaded }}
>
<Collapsible.Trigger class="flex justify-between text-left">
<div class="space-y-0.5">
<h4 class="font-medium leading-4">API Keys</h4>
<p class="text-sm text-muted-foreground">Manage your API keys</p>
</div>
<div class="flex items-center justify-center">
<ChevronRight class={cn('transition-transform', { 'rotate-90': open })} />
</div>
</Collapsible.Trigger>
<Collapsible.Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div {...props} transition:slide class="mt-4 flex flex-col space-y-2">
{#each keys as key}
<Card class="p-2 flex justify-between">
<div>
<h4 class="font-medium text-lg">
{key.name}
</h4>
<p class="text-muted-foreground text-sm">
Created {formatRelative(key.createdAt, new Date())}
{#if key.lastUsed}
∙ Last used {formatRelative(key.lastUsed, new Date())}
{/if}
</p>
</div>
<div class="flex items-center pr-1">
<Confirm
title="Delete API Key"
description="Are you sure you want to delete this API key? This action cannot be undone."
onConfirm={async () => {
await api.user.deleteApiKey.mutate(key.id);
keys = keys.filter((k) => k.id !== key.id);
}}
>
{#snippet triggerContent({ props })}
<Button variant="outline" size="icon" {...props}>
<X />
</Button>
{/snippet}
</Confirm>
</div>
</Card>
{:else}
<p class="pb-2 text-center text-muted-foreground">
No API keys found
</p>
{/each}
<CreateKey bind:keys />
</div>
{/if}
{/snippet}
</Collapsible.Content>
</Collapsible.Root>
{#each keys as key}
<Card class="p-2 flex justify-between">
<div>
<h4 class="font-medium text-lg">
{key.name}
</h4>
<p class="text-muted-foreground text-sm">
Created {formatRelative(key.createdAt, new Date())}
{#if key.lastUsed}
∙ Last used {formatRelative(key.lastUsed, new Date())}
{/if}
</p>
</div>
<div class="flex items-center pr-1">
<Confirm
title="Delete API Key"
description="Are you sure you want to delete this API key? This action cannot be undone."
onConfirm={async () => {
await api.user.deleteApiKey.mutate(key.id);
keys = keys.filter((k) => k.id !== key.id);
}}
>
{#snippet triggerContent({ props })}
<Button variant="outline" size="icon" {...props}>
<X />
</Button>
{/snippet}
</Confirm>
</div>
</Card>
{:else}
<p class="pb-2 text-center text-muted-foreground">No API keys found</p>
{/each}
<CreateKey bind:keys />
</Collapsible>
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import EditPassword from './EditPassword.svelte';
import OAuth from './OAuth.svelte';
import { page } from '$app/stores';
import { page } from '$app/state';
import { Confirm } from '$lib/components/helpers';
import { Button } from '$lib/components/ui/button';
import { api, trpc } from '$lib/trpc';
const user = $derived($page.data.user);
const user = $derived(page.data.user);
const deleteFlights = async () => {
const toastId = toast.loading('Deleting all your flights...');
Expand All @@ -35,7 +35,7 @@
</div>
</div>
<OAuth {user} />
<ApiKeys {user} />
<ApiKeys />
<div class="flex items-center justify-between p-4 rounded-lg border">
<h4 class="font-medium leading-4">Danger zone</h4>
<div>
Expand Down
53 changes: 53 additions & 0 deletions src/lib/components/ui/collapsible/collapsible.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts">
import { ChevronRight } from '@o7/icon/lucide';
import { Collapsible, type WithoutChild } from 'bits-ui';
import type { ClassValue } from 'clsx';
import { slide } from 'svelte/transition';
import { cn } from '$lib/utils';
type Props = WithoutChild<Collapsible.RootProps> & {
title: string;
subtitle?: string;
disabled?: boolean;
className?: ClassValue[];
};
let {
open = $bindable(false),
disabled = false,
title,
subtitle,
class: className,
children,
...restProps
}: Props = $props();
</script>

<Collapsible.Root
bind:open
{disabled}
class={cn('w-full flex flex-col p-4 rounded-lg border', className)}
{...restProps}
>
<Collapsible.Trigger class="flex justify-between text-left">
<div class="space-y-0.5">
<h4 class={cn('font-medium', { 'leading-4': subtitle })}>{title}</h4>
{#if subtitle}
<p class="text-sm text-muted-foreground">{subtitle}</p>
{/if}
</div>
<div class="flex items-center justify-center">
<ChevronRight class={cn('transition-transform', { 'rotate-90': open })} />
</div>
</Collapsible.Trigger>
<Collapsible.Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div {...props} transition:slide class="mt-4 flex flex-col space-y-2">
{@render children?.()}
</div>
{/if}
{/snippet}
</Collapsible.Content>
</Collapsible.Root>
1 change: 1 addition & 0 deletions src/lib/components/ui/collapsible/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Collapsible } from './collapsible.svelte';
1 change: 1 addition & 0 deletions src/lib/db/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type airport = {
*/
continent: 'EU' | 'NA' | 'SA' | 'AS' | 'AF' | 'OC' | 'AN';
country: string;
custom: Generated<boolean>;
};
export type api_key = {
id: Generated<number>;
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/data/airports/airports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type Airport = {
country: string;
iata: string | null;
tz: string;
custom: boolean;
};

export const ensureAirports = async () => {
Expand Down

0 comments on commit ad0d65a

Please sign in to comment.