feat: add label and post management dashboard pages with CRUD functionality
Some checks failed
Frontend CI / build (push) Failing after 1m7s
Some checks failed
Frontend CI / build (push) Failing after 1m7s
This commit is contained in:
parent
4f23151439
commit
300ae2dc05
5
frontend/src/lib/common/framework/ui/InputError.svelte
Normal file
5
frontend/src/lib/common/framework/ui/InputError.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
const { message }: { message?: string | null } = $props();
|
||||
</script>
|
||||
|
||||
<p class="text-sm text-destructive" hidden={!message}>{message}</p>
|
38
frontend/src/lib/common/framework/ui/RestoreButton.svelte
Normal file
38
frontend/src/lib/common/framework/ui/RestoreButton.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/common/framework/components/ui/button';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const {
|
||||
for: htmlFor,
|
||||
defaultValue,
|
||||
postAction,
|
||||
}: {
|
||||
for: string;
|
||||
defaultValue: string;
|
||||
postAction?: () => void;
|
||||
} = $props();
|
||||
|
||||
let hidden = $state(true);
|
||||
|
||||
function onClick() {
|
||||
const input = document.getElementById(htmlFor) as HTMLInputElement | null;
|
||||
if (input) {
|
||||
input.value = defaultValue;
|
||||
input.dispatchEvent(new Event('input'));
|
||||
input.dispatchEvent(new Event('change'));
|
||||
}
|
||||
postAction?.();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const input = document.getElementById(htmlFor) as HTMLInputElement | null;
|
||||
hidden = input?.value === defaultValue;
|
||||
input?.addEventListener('input', () => {
|
||||
hidden = input?.value === defaultValue;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<Button variant="ghost" size="icon-sm" {hidden} onclick={onClick}>
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
</Button>
|
@ -78,6 +78,11 @@ export class ColorViewModel {
|
||||
return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}${toHex(this.alpha)}`;
|
||||
}
|
||||
|
||||
get hexWithoutAlpha(): string {
|
||||
const hexString = this.hex;
|
||||
return hexString.slice(0, 7);
|
||||
}
|
||||
|
||||
private toHsl(): Hsl {
|
||||
const r = this.red / 255;
|
||||
const g = this.green / 255;
|
||||
|
10
frontend/src/lib/label/framework/ui/ColorCode.svelte
Normal file
10
frontend/src/lib/label/framework/ui/ColorCode.svelte
Normal file
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { ColorViewModel } from '$lib/label/adapter/presenter/colorViewModel';
|
||||
|
||||
const { color }: { color: ColorViewModel } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="size-4 rounded-full" style="background-color: {color.hex};"></div>
|
||||
<span class="font-mono text-sm">{color.hex}</span>
|
||||
</div>
|
@ -7,7 +7,7 @@
|
||||
});
|
||||
|
||||
type FormParams = z.infer<typeof formSchema>;
|
||||
export type CreateLabelDialogFormParams = FormParams;
|
||||
export type EditLabelDialogFormParams = FormParams;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@ -24,22 +24,30 @@
|
||||
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||
import { Label as LabelEntity } from '$lib/label/domain/entity/label';
|
||||
import { ColorViewModel } from '$lib/label/adapter/presenter/colorViewModel';
|
||||
import RestoreButton from '$lib/common/framework/ui/RestoreButton.svelte';
|
||||
import InputError from '$lib/common/framework/ui/InputError.svelte';
|
||||
|
||||
const {
|
||||
title,
|
||||
triggerButtonText,
|
||||
disabled,
|
||||
defaultValues = {
|
||||
name: '',
|
||||
color: '#dddddd',
|
||||
},
|
||||
onSubmit: createLabel,
|
||||
}: {
|
||||
title: string;
|
||||
triggerButtonText: string;
|
||||
disabled: boolean;
|
||||
defaultValues?: FormParams;
|
||||
onSubmit: (params: FormParams) => Promise<boolean>;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
let formData = $state<FormParams>({
|
||||
name: '',
|
||||
color: '#dddddd',
|
||||
});
|
||||
let formErrors = $state<Partial<Record<keyof FormParams, string>>>({});
|
||||
let formData: FormParams = $state(defaultValues);
|
||||
let formErrors: Partial<Record<keyof FormParams, string>> = $state({});
|
||||
|
||||
const previewLabel = $derived(
|
||||
LabelViewModel.fromEntity(
|
||||
@ -68,51 +76,60 @@
|
||||
return;
|
||||
}
|
||||
|
||||
formData = {
|
||||
name: '',
|
||||
color: '#dddddd',
|
||||
};
|
||||
formData = defaultValues;
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog bind:open>
|
||||
<DialogTrigger class={buttonVariants({ variant: 'default' })}>Create</DialogTrigger>
|
||||
<DialogTrigger class={buttonVariants({ variant: 'default' })} {disabled}>
|
||||
{triggerButtonText}
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
showCloseButton={false}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeydown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader class="mb-4">
|
||||
<DialogTitle>Create Label</DialogTitle>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form id="create-label-form" onsubmit={onSubmit} class="space-y-3">
|
||||
<div>
|
||||
<Label for="name-input" class="pb-2">Name</Label>
|
||||
<Input
|
||||
id="name-input"
|
||||
type="text"
|
||||
aria-invalid={formErrors.name !== undefined}
|
||||
bind:value={formData.name}
|
||||
/>
|
||||
{#if formErrors.name}
|
||||
<p class="text-sm text-red-500">{formErrors.name}</p>
|
||||
{/if}
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<Input
|
||||
id="name-input"
|
||||
type="text"
|
||||
aria-invalid={formErrors.name !== undefined}
|
||||
bind:value={formData.name}
|
||||
/>
|
||||
<RestoreButton
|
||||
for="name-input"
|
||||
defaultValue={defaultValues.name}
|
||||
postAction={() => (formErrors.name = undefined)}
|
||||
/>
|
||||
</div>
|
||||
<InputError message={formErrors.name} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="w-fit">
|
||||
<Label for="color-input" class="pb-2">Color</Label>
|
||||
<Input
|
||||
id="color-input"
|
||||
type="color"
|
||||
class="w-16"
|
||||
aria-invalid={formErrors.color !== undefined}
|
||||
bind:value={formData.color}
|
||||
/>
|
||||
{#if formErrors.color}
|
||||
<p class="text-sm text-red-500">{formErrors.color}</p>
|
||||
{/if}
|
||||
<div class="flex flex-row items-center gap-x-2">
|
||||
<Input
|
||||
id="color-input"
|
||||
type="color"
|
||||
class="w-16"
|
||||
aria-invalid={formErrors.color !== undefined}
|
||||
bind:value={formData.color}
|
||||
/>
|
||||
<RestoreButton
|
||||
for="color-input"
|
||||
defaultValue={defaultValues.color}
|
||||
postAction={() => (formErrors.color = undefined)}
|
||||
/>
|
||||
</div>
|
||||
<InputError message={formErrors.color} />
|
||||
</div>
|
||||
</form>
|
||||
|
@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { LabelLoadedStore } from '$lib/label/adapter/presenter/labelLoadedStore';
|
||||
import ColorCode from '$lib/label/framework/ui/ColorCode.svelte';
|
||||
import EditLabelDialog, {
|
||||
type EditLabelDialogFormParams,
|
||||
} from '$lib/label/framework/ui/EditLabelDialog.svelte';
|
||||
import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
|
||||
const { id }: { id: number } = $props();
|
||||
|
||||
const labelLoadedStore = getContext<LabelLoadedStore>(LabelLoadedStore.name);
|
||||
const labelLoadedState = $derived($labelLoadedStore);
|
||||
const { trigger: loadLabel } = labelLoadedStore;
|
||||
const label = $derived(labelLoadedState.data);
|
||||
|
||||
const formDefaultValues: EditLabelDialogFormParams | null = $derived.by(() => {
|
||||
if (labelLoadedState.data === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: labelLoadedState.data.name,
|
||||
color: labelLoadedState.data.color.hexWithoutAlpha,
|
||||
};
|
||||
});
|
||||
|
||||
async function onSubmit(params: EditLabelDialogFormParams): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
onMount(() => loadLabel(id));
|
||||
</script>
|
||||
|
||||
<div class="dashboard-container mb-10">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h1 class="py-16 text-5xl font-bold text-gray-800">Label Details</h1>
|
||||
<EditLabelDialog
|
||||
title="Update Label"
|
||||
triggerButtonText="Edit"
|
||||
disabled={!labelLoadedState.isSuccess()}
|
||||
defaultValues={formDefaultValues ?? undefined}
|
||||
{onSubmit}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-8 gap-y-3">
|
||||
<span class="font-medium whitespace-nowrap">ID</span>
|
||||
<span>{label?.id}</span>
|
||||
|
||||
<span class="font-medium whitespace-nowrap">Name</span>
|
||||
<span>{label?.name}</span>
|
||||
|
||||
<span class="font-medium whitespace-nowrap">Color</span>
|
||||
<div class="content-center">
|
||||
{#if label}
|
||||
<ColorCode color={label.color} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<span class="font-medium whitespace-nowrap">Preview</span>
|
||||
<div class="content-center">
|
||||
{#if label}
|
||||
<PostLabel {label} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,19 +1,18 @@
|
||||
<script lang="ts">
|
||||
import TableBody from '$lib/common/framework/components/ui/table/table-body.svelte';
|
||||
import TableCell from '$lib/common/framework/components/ui/table/table-cell.svelte';
|
||||
import TableHead from '$lib/common/framework/components/ui/table/table-head.svelte';
|
||||
import TableHeader from '$lib/common/framework/components/ui/table/table-header.svelte';
|
||||
import TableRow from '$lib/common/framework/components/ui/table/table-row.svelte';
|
||||
import Table from '$lib/common/framework/components/ui/table/table.svelte';
|
||||
import { LabelCreatedStore } from '$lib/label/adapter/presenter/labelCreatedStore';
|
||||
import { LabelsListedStore } from '$lib/label/adapter/presenter/labelsListedStore';
|
||||
import CreateLabelDialog, {
|
||||
type CreateLabelDialogFormParams,
|
||||
} from '$lib/label/framework/ui/CreateLabelDialog.svelte';
|
||||
import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
|
||||
import EditLabelDialog, {
|
||||
type EditLabelDialogFormParams,
|
||||
} from '$lib/label/framework/ui/EditLabelDialog.svelte';
|
||||
import { ColorViewModel } from '$lib/label/adapter/presenter/colorViewModel';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import LabelOverallDashboardTabelRow from '$lib/label/framework/ui/LabelOverallDashboardTabelRow.svelte';
|
||||
|
||||
const labelCreatedStore = getContext<LabelCreatedStore>(LabelCreatedStore.name);
|
||||
const labelCreatedState = $derived($labelCreatedStore);
|
||||
@ -23,7 +22,7 @@
|
||||
const labelsListedState = $derived($labelsListedStore);
|
||||
const { trigger: loadLabels } = labelsListedStore;
|
||||
|
||||
async function onCreateLabelDialogSubmit(params: CreateLabelDialogFormParams): Promise<boolean> {
|
||||
async function onSubmit(params: EditLabelDialogFormParams): Promise<boolean> {
|
||||
const colorViewModel = ColorViewModel.fromHex(params.color);
|
||||
const color = colorViewModel.toEntity();
|
||||
|
||||
@ -51,9 +50,11 @@
|
||||
<div class="dashboard-container mb-10">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h1 class="py-16 text-5xl font-bold text-gray-800">Label</h1>
|
||||
<CreateLabelDialog
|
||||
<EditLabelDialog
|
||||
title="Create Label"
|
||||
triggerButtonText="Create"
|
||||
disabled={labelCreatedState.isLoading()}
|
||||
onSubmit={onCreateLabelDialogSubmit}
|
||||
{onSubmit}
|
||||
/>
|
||||
</div>
|
||||
<Table>
|
||||
@ -68,21 +69,7 @@
|
||||
<TableBody>
|
||||
{#if labelsListedState.isSuccess()}
|
||||
{#each labelsListedState.data as label (label.id)}
|
||||
<TableRow>
|
||||
<TableCell>{label.id}</TableCell>
|
||||
<TableCell><span class="text-wrap">{label.name}</span></TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="size-4 rounded-full" style="background-color: {label.color.hex};"></div>
|
||||
<span class="font-mono text-sm">{label.color.hex}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<PostLabel {label} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<LabelOverallDashboardTabelRow {label} />
|
||||
{/each}
|
||||
{/if}
|
||||
</TableBody>
|
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { TableCell, TableRow } from '$lib/common/framework/components/ui/table';
|
||||
import type { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||
import ColorCode from '$lib/label/framework/ui/ColorCode.svelte';
|
||||
import PostLabel from '$lib/label/framework/ui/PostLabel.svelte';
|
||||
|
||||
const { label }: { label: LabelViewModel } = $props();
|
||||
</script>
|
||||
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<a href={`/dashboard/label/${label.id}`} class="font-medium underline">
|
||||
{label.id}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell><span class="text-wrap">{label.name}</span></TableCell>
|
||||
<TableCell>
|
||||
<ColorCode color={label.color} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<PostLabel {label} />
|
||||
</TableCell>
|
||||
</TableRow>
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/container';
|
||||
import { ImageUploadedStore } from '$lib/image/adapter/presenter/imageUploadedStore';
|
||||
import ImageManagementPage from '$lib/image/framework/ui/ImageManagementPage.svelte';
|
||||
import ImageOverallDashboardPage from '$lib/image/framework/ui/ImageOverallDashboardPage.svelte';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
const container = getContext<Container>(Container.name);
|
||||
@ -9,4 +9,4 @@
|
||||
setContext(ImageUploadedStore.name, store);
|
||||
</script>
|
||||
|
||||
<ImageManagementPage />
|
||||
<ImageOverallDashboardPage />
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Container } from '$lib/container';
|
||||
import { LabelCreatedStore } from '$lib/label/adapter/presenter/labelCreatedStore';
|
||||
import { LabelsListedStore } from '$lib/label/adapter/presenter/labelsListedStore';
|
||||
import LabelManagementPage from '$lib/label/framework/ui/LabelManagementPage.svelte';
|
||||
import LabelOverallDashboardPage from '$lib/label/framework/ui/LabelOverallDashboardPage.svelte';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||
@ -18,4 +18,4 @@
|
||||
setContext(LabelsListedStore.name, labelsListedStore);
|
||||
</script>
|
||||
|
||||
<LabelManagementPage />
|
||||
<LabelOverallDashboardPage />
|
||||
|
@ -4,14 +4,17 @@
|
||||
import { Container } from '$lib/container';
|
||||
import { LabelViewModel } from '$lib/label/adapter/presenter/labelViewModel';
|
||||
import { LabelLoadedStore } from '$lib/label/adapter/presenter/labelLoadedStore';
|
||||
import LabelContentDashboardPage from '$lib/label/framework/ui/LabelContentDashboardPage.svelte';
|
||||
|
||||
const { data, params }: PageProps = $props();
|
||||
const { id } = params;
|
||||
const numericId = $derived(Number(id));
|
||||
|
||||
const container = getContext<Container>(Container.name);
|
||||
|
||||
const initialData = LabelViewModel.rehydrate(data.dehydratedData!);
|
||||
const initialData = LabelViewModel.rehydrate(data.dehydratedData);
|
||||
const store = container.createLabelLoadedStore(initialData);
|
||||
setContext(LabelLoadedStore.name, store);
|
||||
</script>
|
||||
|
||||
<div>{id}</div>
|
||||
<LabelContentDashboardPage id={numericId} />
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Container } from '$lib/container';
|
||||
import { PostCreatedStore } from '$lib/post/adapter/presenter/postCreatedStore';
|
||||
import { PostsListedStore } from '$lib/post/adapter/presenter/postsListedStore';
|
||||
import PostManagementPage from '$lib/post/framework/ui/PostManagementPage.svelte';
|
||||
import PostOverallDashboardPage from '$lib/post/framework/ui/PostOverallDashboardPage.svelte';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||
@ -18,4 +18,4 @@
|
||||
setContext(PostsListedStore.name, postsListedStore);
|
||||
</script>
|
||||
|
||||
<PostManagementPage />
|
||||
<PostOverallDashboardPage />
|
||||
|
@ -10,7 +10,7 @@
|
||||
const { id } = params;
|
||||
const container = getContext<Container>(Container.name);
|
||||
|
||||
const initialData = PostViewModel.rehydrate(data.dehydratedData!);
|
||||
const initialData = PostViewModel.rehydrate(data.dehydratedData);
|
||||
const store = container.createPostLoadedStore(initialData);
|
||||
setContext(PostLoadedStore.name, store);
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user