change: remove loading while request recipe

progress: WIP editing flow

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-03-24 17:52:53 +07:00
parent 3388eca2fe
commit e9192c8607
24 changed files with 538 additions and 81 deletions

View file

@ -112,7 +112,14 @@ export const columns: ColumnDef<RecipelistMaterial>[] = [
row_uid: row.original.id, row_uid: row.original.id,
mat_id: row.original.material_id, mat_id: row.original.material_id,
onEditValue: (changes: any) => { onEditValue: (changes: any) => {
recipeDataEvent.set({
event_type: 'edit_change_value_rpl',
payload: changes,
index: row.original.id
});
// get change parameters // get change parameters
row.toggleSelected(row.original.is_use);
}, },
onDetectMixOrder: () => { onDetectMixOrder: () => {
// set next // set next

View file

@ -20,13 +20,23 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { addNotification } from '$lib/core/stores/noti'; import { addNotification } from '$lib/core/stores/noti';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { ValueEvent } from './value_event'; import { actionReport, ValueEvent } from './value_event';
import ScrollArea from '../ui/scroll-area/scroll-area.svelte'; import ScrollArea from '../ui/scroll-area/scroll-area.svelte';
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
import { auth } from '$lib/core/stores/auth';
import { departmentStore } from '$lib/core/stores/departments';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import type { setDefaultAutoSelectFamily } from 'node:net';
let { row_id }: { row_id: number } = $props(); let { row_id }: { row_id: number } = $props();
let current_editing_data: any = $state(); let current_editing_data: any = $state();
let changed_data: any = $state(); let changed_data: any = $state({});
let sheetOpenState = $state(false);
let currentRef = $state('');
let warnUserNotSaveChange = $state(false);
// -------------------------------------------------- // --------------------------------------------------
@ -76,7 +86,7 @@
// update value, do re-render // update value, do re-render
if (event.payload) { if (event.payload) {
current_editing_data = event.payload; current_editing_data = event.payload;
console.log(`GET requested data: ${JSON.stringify(current_editing_data)}`); // console.log(`GET requested data: ${JSON.stringify(current_editing_data)}`);
// default topping // default topping
if ( if (
@ -105,11 +115,15 @@
current_editing_data.water.yield > 0 || current_editing_data.water.cold > 0; current_editing_data.water.yield > 0 || current_editing_data.water.cold > 0;
toggledOpenFeed = toggledOpenFeed =
current_editing_data.feed.pattern > 0 || current_editing_data.feed.parameter > 0; current_editing_data.feed.pattern > 0 || current_editing_data.feed.parameter > 0;
sheetOpenState = true;
} }
} }
} }
function requestDataFromDisplay() { function requestDataFromDisplay() {
// pause show until have data
sheetOpenState = false;
console.log('sending request edit', row_id); console.log('sending request edit', row_id);
recipeDataEvent.set({ recipeDataEvent.set({
event_type: 'edit_mat_field', event_type: 'edit_mat_field',
@ -120,13 +134,12 @@
setTimeout(() => { setTimeout(() => {
if (current_editing_data === undefined) { if (current_editing_data === undefined) {
addNotification('ERR:Unable to edit'); addNotification('ERR:Unable to edit');
sheetOpenState = false;
} }
}, 5000); }, 5000);
} }
function onFieldValueChange(field_name: string[], new_value: any) { function onFieldValueChange(field_name: string[], new_value: any) {
console.log('change on', JSON.stringify(field_name), new_value);
// validate if field value changes // validate if field value changes
let curr_val; let curr_val;
for (let field of field_name) { for (let field of field_name) {
@ -147,43 +160,133 @@
if (has_changed) { if (has_changed) {
if (value_event_state === ValueEvent.NONE) { if (value_event_state === ValueEvent.NONE) {
value_event_state = ValueEvent.EDITED; value_event_state = ValueEvent.EDITED;
// save change
let single_key = '';
for (let field of field_name) {
single_key += field + '_';
}
single_key = single_key.slice(0, single_key.length - 1);
console.log('save to key', single_key);
changed_data[single_key] = new_value;
} }
// save change
let single_key = '';
for (let field of field_name) {
single_key += field + '_';
}
single_key = single_key.slice(0, single_key.length - 1);
changed_data[single_key] = new_value;
} else { } else {
// revert value in key // revert value in key
} }
} }
function saveEditingValue() { function saveEditingValue() {
console.log('saving value ...'); console.log('saving value ...', value_event_state);
if (value_event_state === ValueEvent.EDITED) { if (value_event_state === ValueEvent.EDITED) {
let payload = {
source: current_editing_data,
change: changed_data
};
recipeDataEvent.set({ recipeDataEvent.set({
event_type: 'save_mat_field', event_type: 'save_mat_field',
payload: current_editing_data, payload,
index: row_id index: row_id
}); });
value_event_state = ValueEvent.SAVED;
actionReport(
'change_mat_field',
{
index: row_id,
...payload
},
currentRef
);
sheetOpenState = false;
} else {
// set noti
addNotification(`WARN:Cannot save, please retry ...`);
} }
} }
function handleToppingGroupChange(v: any) { function handleToppingGroupChange(v: any) {
console.log('change topping group', JSON.stringify(v)); console.log('change topping group');
selected_category_id = v.groupID; selected_category_id = v.groupID;
// get default // get default
selected_topping_list_id = v.idDefault ?? 0; selected_topping_list_id = v.idDefault ?? 0;
// set to edit state even if selected same group again
value_event_state = ValueEvent.EDITED;
if (current_editing_data['toppings']) {
let toppings_length = current_editing_data['toppings'].length;
if (!changed_data['toppings']) {
changed_data['toppings'] = new Array<any>(toppings_length);
// console.log('filling change topping', JSON.stringify(changed_data));
}
}
let idx = getToppingSlotIndex(current_editing_data['mat_id']);
// get old state topping idx
let current_selection = current_editing_data['toppings'][idx];
// let default_from_recipe = current_editing_data['toppings'][idx]['ListGroupID'][0];
// console.log(`Current TG: `, JSON.stringify(current_selection));
if (current_selection['groupID'] !== undefined || current_selection['groupID'] !== null) {
try {
changed_data['toppings'][idx] = current_selection;
changed_data['toppings'][idx]['groupID'] = `${selected_category_id}`;
changed_data['toppings'][idx]['defaultIDSelect'] = selected_topping_list_id;
changed_data['toppings'][idx]['ListGroupID'][0] = selected_topping_list_id;
} catch (topping_group_exception) {
console.error('Error on topping group select', topping_group_exception);
}
}
} }
function handleToppingListChange(v: any) { function handleToppingListChange(v: any) {
console.log('Topping list chose: ', JSON.stringify(v)); console.log('Topping list chose ');
selected_topping_list_id = v.id; selected_topping_list_id = v.id;
// set to edit state even if selected same group again
value_event_state = ValueEvent.EDITED;
let idx = getToppingSlotIndex(current_editing_data['mat_id']);
if (current_editing_data['toppings']) {
let toppings_length = current_editing_data['toppings'].length;
// case: topping not init
if (!changed_data['toppings']) {
changed_data['toppings'] = new Array<any>(toppings_length);
}
}
let current_selection = current_editing_data['toppings'][idx];
if (current_selection['groupID'] !== undefined || current_selection['groupID'] !== null) {
try {
changed_data['toppings'][idx] = current_selection;
// changed_data['toppings'][idx]['groupID'] = `${selected_category_id}`;
changed_data['toppings'][idx]['defaultIDSelect'] = selected_topping_list_id;
changed_data['toppings'][idx]['ListGroupID'][0] = selected_topping_list_id;
} catch (topping_list_exception) {
console.error('Error on topping group select', topping_list_exception);
}
}
}
function beforeClosing() {
if (value_event_state === ValueEvent.EDITED) {
if (warnUserNotSaveChange) {
// Discard all
warnUserNotSaveChange = false;
// sheetOpenState = false;
return true;
} else {
// show no save warning
warnUserNotSaveChange = true;
return false;
}
} else {
sheetOpenState = false;
return true;
}
} }
// recipelist-value-editor.svelte?t=1773541064272:57 GET requested data: {"mat_id":1002,"mat_type":"bean","mat_name":"medium-roasts (1002)","params":{"esp-v2-press-value":"24"},"toppings":[{"ListGroupID":[1,0,0,0],"defaultIDSelect":1,"groupID":"1","isUse":true},{"ListGroupID":[6,0,0,0],"defaultIDSelect":31,"groupID":"6","isUse":true},{"ListGroupID":[7,0,0,0],"defaultIDSelect":33,"groupID":"7","isUse":true},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[530,0,0,0],"defaultIDSelect":532,"groupID":"530","isUse":true},{"ListGroupID":[500,0,0,0],"defaultIDSelect":502,"groupID":"500","isUse":true}],"current_topping_list":[],"has_mix_ord":false,"feed":{"pattern":0,"parameter":0},"water":{"cold":0,"yield":30},"powder":{"gram":7,"time":9},"syrup":{"gram":0,"time":0},"stir_time":120} // recipelist-value-editor.svelte?t=1773541064272:57 GET requested data: {"mat_id":1002,"mat_type":"bean","mat_name":"medium-roasts (1002)","params":{"esp-v2-press-value":"24"},"toppings":[{"ListGroupID":[1,0,0,0],"defaultIDSelect":1,"groupID":"1","isUse":true},{"ListGroupID":[6,0,0,0],"defaultIDSelect":31,"groupID":"6","isUse":true},{"ListGroupID":[7,0,0,0],"defaultIDSelect":33,"groupID":"7","isUse":true},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[530,0,0,0],"defaultIDSelect":532,"groupID":"530","isUse":true},{"ListGroupID":[500,0,0,0],"defaultIDSelect":502,"groupID":"500","isUse":true}],"current_topping_list":[],"has_mix_ord":false,"feed":{"pattern":0,"parameter":0},"water":{"cold":0,"yield":30},"powder":{"gram":7,"time":9},"syrup":{"gram":0,"time":0},"stir_time":120}
@ -192,6 +295,9 @@
// recipelist-value-editor.svelte?t=1773541064272:57 GET requested data: {"mat_id":9501,"mat_type":"cup","mat_name":"CUP paper (9501)","params":{},"toppings":[{"ListGroupID":[1,0,0,0],"defaultIDSelect":1,"groupID":"1","isUse":true},{"ListGroupID":[6,0,0,0],"defaultIDSelect":31,"groupID":"6","isUse":true},{"ListGroupID":[7,0,0,0],"defaultIDSelect":33,"groupID":"7","isUse":true},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[530,0,0,0],"defaultIDSelect":532,"groupID":"530","isUse":true},{"ListGroupID":[500,0,0,0],"defaultIDSelect":502,"groupID":"500","isUse":true}],"current_topping_list":[],"has_mix_ord":false,"feed":{"pattern":0,"parameter":0},"water":{"cold":0,"yield":0},"powder":{"gram":0,"time":0},"syrup":{"gram":0,"time":0},"stir_time":0} // recipelist-value-editor.svelte?t=1773541064272:57 GET requested data: {"mat_id":9501,"mat_type":"cup","mat_name":"CUP paper (9501)","params":{},"toppings":[{"ListGroupID":[1,0,0,0],"defaultIDSelect":1,"groupID":"1","isUse":true},{"ListGroupID":[6,0,0,0],"defaultIDSelect":31,"groupID":"6","isUse":true},{"ListGroupID":[7,0,0,0],"defaultIDSelect":33,"groupID":"7","isUse":true},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[530,0,0,0],"defaultIDSelect":532,"groupID":"530","isUse":true},{"ListGroupID":[500,0,0,0],"defaultIDSelect":502,"groupID":"500","isUse":true}],"current_topping_list":[],"has_mix_ord":false,"feed":{"pattern":0,"parameter":0},"water":{"cold":0,"yield":0},"powder":{"gram":0,"time":0},"syrup":{"gram":0,"time":0},"stir_time":0}
onMount(() => { onMount(() => {
sheetOpenState = false;
console.log('sheet open? ', sheetOpenState);
let refFrom = get(referenceFromPage); let refFrom = get(referenceFromPage);
categories = get( categories = get(
refFrom === 'overview' ? toppingGroupFromServerQuery : toppingGroupFromMachineQuery refFrom === 'overview' ? toppingGroupFromServerQuery : toppingGroupFromMachineQuery
@ -200,6 +306,9 @@
refFrom === 'overview' ? toppingListFromServerQuery : toppingListFromMachineQuery refFrom === 'overview' ? toppingListFromServerQuery : toppingListFromMachineQuery
); );
// save ref
currentRef = refFrom;
return recipeDataEvent.subscribe((event) => { return recipeDataEvent.subscribe((event) => {
if (event !== null && event.index !== undefined && event.index === row_id) { if (event !== null && event.index !== undefined && event.index === row_id) {
handleEvents(event); handleEvents(event);
@ -208,7 +317,16 @@
}); });
</script> </script>
<Sheet.Root> <Sheet.Root
bind:open={sheetOpenState}
onOpenChange={(next) => {
if (!next) {
beforeClosing();
} else {
sheetOpenState = true;
}
}}
>
<Sheet.Trigger> <Sheet.Trigger>
<Button <Button
variant="default" variant="default"
@ -256,6 +374,15 @@
</Field.Set> </Field.Set>
<ScrollArea class="h-[60vh] w-full" type="always"> <ScrollArea class="h-[60vh] w-full" type="always">
{#if warnUserNotSaveChange}
<div class="flex flex-col items-center gap-2 text-center">
<h1 class="text-lg font-bold text-red-600">Unsaved changes may be lost.</h1>
<p class="text-balance text-red-700">
Click on "Discard Changes" to revert back values
</p>
</div>
{/if}
<!-- topping layout --> <!-- topping layout -->
<div <div
class={current_editing_data.mat_type === 'topping' class={current_editing_data.mat_type === 'topping'
@ -487,10 +614,13 @@
</Field.Group> </Field.Group>
</Field.Set> </Field.Set>
</ScrollArea> </ScrollArea>
<!-- final --> <!-- final -->
<Field.Field orientation="horizontal"> <Field.Field orientation="horizontal">
<Button type="button" onclick={() => saveEditingValue()}>Save</Button> <Button type="button" onclick={() => saveEditingValue()}>Save</Button>
<Button variant="outline" type="button">Cancel</Button> <Button variant="outline" type="button" onclick={() => beforeClosing()}
>{warnUserNotSaveChange ? 'Discard Changes' : 'Cancel'}</Button
>
</Field.Field> </Field.Field>
</Field.Group> </Field.Group>
</form> </form>

View file

@ -170,7 +170,7 @@
if (currentToppings[getToppingSlot()]['ListGroupID'][0] === 0) { if (currentToppings[getToppingSlot()]['ListGroupID'][0] === 0) {
return 'Empty'; return 'Empty';
} }
if (!current_selected) { if (current_selected === undefined || current_selected === null) {
return 'Unknown'; return 'Unknown';
} }
@ -237,11 +237,14 @@
}, },
index: row_uid index: row_uid
}); });
} } else if (event.event_type === 'save_mat_field') {
} console.log('receive saving process mat, do refresh...');
let change_values = event.payload.change;
function triggerEditChange(value: any) { // apply now
console.log('triggered on change editing', JSON.stringify(value)); let keys = Object.keys(change_values);
console.log('change keys', JSON.stringify(keys));
}
} }
onMount(() => { onMount(() => {

View file

@ -1,6 +1,41 @@
import { get } from 'svelte/store';
import { auth } from '$lib/core/stores/auth';
import { departmentStore } from '$lib/core/stores/departments';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import { addNotification } from '$lib/core/stores/noti';
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
enum ValueEvent { enum ValueEvent {
NONE, NONE,
EDITED EDITED,
SAVED
} }
export { ValueEvent }; function actionReport(action_name: string, values: any, currentRef: string) {
let country = get(departmentStore) ?? 'unknown dep';
if (currentRef === 'brew') {
// from machine
let machine_info = get(machineInfoStore);
if (machine_info) {
let bid = machine_info.boxId ?? 'BOX_UNK';
let cc = machine_info.country;
country = `${bid}-${cc}`;
} else {
addNotification('WARN:Saving as unknown department, please check setting on machine');
}
}
sendMessage({
type: 'log_report',
payload: {
user: get(auth)?.email ?? 'unknown',
action: action_name,
country,
values
}
});
}
export { ValueEvent, actionReport };

View file

@ -48,7 +48,7 @@
// currentData.recipes = newChange.value; // currentData.recipes = newChange.value;
// //
// TODO: build into structure, flatten fields into 1 layer, strip off `id` (row id) // TODO: build into structure, flatten fields into 1 layer, strip off `id` (row id)
console.log(newChange); console.log('pending change recipe list', newChange);
} }
// await adb.push('/sdcard/coffeevending/.curr.brewing.json', JSON.stringify(currentData)); // await adb.push('/sdcard/coffeevending/.curr.brewing.json', JSON.stringify(currentData));
@ -103,4 +103,6 @@
<RecipeDetail recipeData={currentData} {onPendingChange} {refPage} /> <RecipeDetail recipeData={currentData} {onPendingChange} {refPage} />
</Dialog.Content> </Dialog.Content>
</Dialog.Root> </Dialog.Root>
{:else}{/if} {:else}
<!-- TODO: handle case open on mobile? -->
{/if}

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import {
buttonVariants,
type ButtonVariant,
type ButtonSize,
} from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
variant = "default",
size = "default",
...restProps
}: AlertDialogPrimitive.ActionProps & {
variant?: ButtonVariant;
size?: ButtonSize;
} = $props();
</script>
<AlertDialogPrimitive.Action
bind:ref
data-slot="alert-dialog-action"
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-action", className)}
{...restProps}
/>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import {
buttonVariants,
type ButtonVariant,
type ButtonSize,
} from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
variant = "outline",
size = "default",
...restProps
}: AlertDialogPrimitive.CancelProps & {
variant?: ButtonVariant;
size?: ButtonSize;
} = $props();
</script>
<AlertDialogPrimitive.Cancel
bind:ref
data-slot="alert-dialog-cancel"
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-cancel", className)}
{...restProps}
/>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import AlertDialogPortal from "./alert-dialog-portal.svelte";
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
size = "default",
portalProps,
...restProps
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
size?: "default" | "sm";
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
} = $props();
</script>
<AlertDialogPortal {...portalProps}>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
bind:ref
data-slot="alert-dialog-content"
data-size={size}
class={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none",
className
)}
{...restProps}
/>
</AlertDialogPortal>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.DescriptionProps = $props();
</script>
<AlertDialogPrimitive.Description
bind:ref
data-slot="alert-dialog-description"
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)}
{...restProps}
/>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-footer"
class={cn(
"bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-header"
class={cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-dialog-media"
class={cn("bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.OverlayProps = $props();
</script>
<AlertDialogPrimitive.Overlay
bind:ref
data-slot="alert-dialog-overlay"
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props();
</script>
<AlertDialogPrimitive.Portal {...restProps} />

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AlertDialogPrimitive.TitleProps = $props();
</script>
<AlertDialogPrimitive.Title
bind:ref
data-slot="alert-dialog-title"
class={cn("text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
</script>
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
</script>
<AlertDialogPrimitive.Root bind:open {...restProps} />

View file

@ -0,0 +1,40 @@
import Root from "./alert-dialog.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
import Media from "./alert-dialog-media.svelte";
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Media,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
Media as AlertDialogMedia,
};

View file

@ -4,25 +4,25 @@
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({ export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs", default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
destructive: outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
outline: ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs", destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
icon: "size-9", lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
"icon-sm": "size-8", icon: "size-8",
"icon-lg": "size-10", "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
}, },
}, },
defaultVariants: { defaultVariants: {

View file

@ -35,7 +35,7 @@ const handlers: Record<string, (payload: any) => void> = {
if (stream_id) { if (stream_id) {
addNotification('INFO:Start streaming data'); addNotification('INFO:Start streaming data');
recipeLoading.set(true); // recipeLoading.set(true);
recipeStreamMeta.set({ recipeStreamMeta.set({
id: stream_id, id: stream_id,
total_size: total_size, total_size: total_size,
@ -82,6 +82,7 @@ const handlers: Record<string, (payload: any) => void> = {
if (percent == 100) { if (percent == 100) {
addNotification(`INFO:Current progress ${percent}%`); addNotification(`INFO:Current progress ${percent}%`);
} }
buildOverviewFromServer();
} }
} }
}, },

View file

@ -9,6 +9,8 @@ export function sendMessage(msg: OutMessage): boolean {
const socket = get(socketStore); const socket = get(socketStore);
const data = JSON.stringify(msg); const data = JSON.stringify(msg);
// console.log('try sending ', data);
if (!socket || socket.readyState !== WebSocket.OPEN) { if (!socket || socket.readyState !== WebSocket.OPEN) {
console.warn('WebSocket not connected, put to queue'); console.warn('WebSocket not connected, put to queue');

View file

@ -7,49 +7,56 @@ import { auth } from '../client/firebase';
import { addNotification } from './noti'; import { addNotification } from './noti';
let socket: WebSocket | null = null; let socket: WebSocket | null = null;
const ENABLE_WS_DEBUG: boolean = false;
export const socketStore = writable<WebSocket | null>(null); export const socketStore = writable<WebSocket | null>(null);
export function connectToWebsocket() { export function connectToWebsocket() {
if (browser) { if (browser) {
console.log('connecting to ', env.PUBLIC_WSS); // console.log('connecting to ', env.PUBLIC_WSS);
socket = new WebSocket(`${env.PUBLIC_WSS}`); try {
socket = new WebSocket(`${env.PUBLIC_WSS}`);
socket.addEventListener('open', () => { socket.addEventListener('open', () => {
socketStore.set(socket); socketStore.set(socket);
addNotification('INFO:Connected!'); addNotification('INFO:Connected!');
// recover messages on connect, flushing // recover messages on connect, flushing
while (get(msgQueue).length) { while (get(msgQueue).length) {
let queue = get(msgQueue); let queue = get(msgQueue);
let current = queue.shift(); let current = queue.shift();
if (current && socket) { if (current && socket) {
socket.send(current); socket.send(current);
// set next // set next
msgQueue.set(queue); msgQueue.set(queue);
}
} }
});
socket.addEventListener('message', (event) => {
handleIncomingMessages(event.data);
});
socket.addEventListener('close', () => {
socketStore.set(null);
socket = null;
if (auth.currentUser) {
// console.log('try reconnect websocket ...');
// retry again
setTimeout(() => connectToWebsocket(), 5000);
}
});
socket.addEventListener('error', (e) => {
// console.log('WebSocket error: ', e);
socketStore.set(null);
});
} catch (socket_error: any) {
if (ENABLE_WS_DEBUG) {
console.error('WS_ERR', socket_error);
} }
}); }
socket.addEventListener('message', (event) => {
handleIncomingMessages(event.data);
});
socket.addEventListener('close', () => {
socketStore.set(null);
socket = null;
if (auth.currentUser) {
console.log('try reconnect websocket ...');
// retry again
setTimeout(() => connectToWebsocket(), 5000);
}
});
socket.addEventListener('error', (e) => {
console.log('WebSocket error: ', e);
socketStore.set(null);
});
return () => { return () => {
if (socket?.readyState === WebSocket.OPEN) { if (socket?.readyState === WebSocket.OPEN) {

View file

@ -22,4 +22,13 @@ export type OutMessage =
permissions: string; permissions: string;
}; };
}; };
}
| {
type: 'log_report';
payload: {
user: string;
action: string;
country: string;
values: any;
};
}; };

View file

@ -50,11 +50,11 @@
// schedule check if recipe is empty // schedule check if recipe is empty
if (data.recipes.length == 0) { if (data.recipes.length == 0) {
console.log('loading recipe ....'); console.log('loading recipe ....');
recipeLoading.set(true); // recipeLoading.set(true);
// empty // empty
await getRecipes(); await getRecipes();
setTimeout(() => recipeLoading.set(false), 3000); // setTimeout(() => recipeLoading.set(false), 3000);
} }
}, 30000); }, 30000);
return () => { return () => {