Standardize Core UI forms and unify schemas for V3 API compatibility
- Implement new Svelte 5 Person, Address, and Contact form components with surgical payload logic. - Refactor core routes (People, Addresses, Contacts) to support unified Create/Edit workflows. - Update ae_types.ts, db_core.ts, and db_journals.ts to align with V3 backend object models. - Fix type safety issues in Journal history views and refine metadata display. - Migrate person core functions to the newer ae_core__person module.
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { MapPin, Plus, Search, ExternalLink } from 'lucide-svelte';
|
||||
import { MapPin, Plus, Search, ExternalLink, X } from 'lucide-svelte';
|
||||
import { load_ae_obj_li__address, create_ae_obj__address } from '$lib/ae_core/ae_core__address';
|
||||
import Address_form from './ae_comp__address_form.svelte';
|
||||
|
||||
let address_li: any[] = $state([]);
|
||||
let loading = $state(true);
|
||||
let show_add_form = $state(false);
|
||||
|
||||
async function load_addresses() {
|
||||
if (!$ae_loc.account_id) return;
|
||||
@@ -20,27 +22,6 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handle_add() {
|
||||
const city = prompt('Enter city:');
|
||||
if (!city) return;
|
||||
const state_province = prompt('Enter state/province:');
|
||||
const country = prompt('Enter country:', 'USA');
|
||||
|
||||
const result = await create_ae_obj__address({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
data_kv: { city, state_province, country, enable: true },
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
if (result) {
|
||||
load_addresses();
|
||||
if (result.address_id_random) {
|
||||
goto(`/core/addresses/${result.address_id_random}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!$ae_loc.manager_access) {
|
||||
goto('/core');
|
||||
@@ -56,11 +37,30 @@
|
||||
<MapPin size={24} />
|
||||
<h1 class="h2">Address Management</h1>
|
||||
</div>
|
||||
<button class="btn variant-filled-primary" onclick={handle_add}>
|
||||
<Plus size={16} class="mr-2" /> Add Address
|
||||
<button class="btn variant-filled-primary" onclick={() => show_add_form = !show_add_form}>
|
||||
{#if show_add_form}
|
||||
<X size={16} class="mr-2" /> Cancel
|
||||
{:else}
|
||||
<Plus size={16} class="mr-2" /> Add Address
|
||||
{/if}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if show_add_form}
|
||||
<div class="mb-8">
|
||||
<Address_form
|
||||
onSave={(new_addr) => {
|
||||
show_add_form = false;
|
||||
load_addresses();
|
||||
if (new_addr.address_id_random) {
|
||||
goto(`/core/addresses/${new_addr.address_id_random}`);
|
||||
}
|
||||
}}
|
||||
onCancel={() => show_add_form = false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="placeholder animate-pulse h-64 w-full"></div>
|
||||
{:else if address_li.length === 0}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
import { editable_fields__address } from '$lib/ae_core/ae_core__address.editable_fields';
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Save, Trash2, ArrowLeft, MapPin } from 'lucide-svelte';
|
||||
import { Save, Trash2, ArrowLeft, MapPin, Edit, Eye } from 'lucide-svelte';
|
||||
import Address_form from '../ae_comp__address_form.svelte';
|
||||
|
||||
let address_id = $page.params.address_id;
|
||||
let address: any = $state(null);
|
||||
let loading = $state(true);
|
||||
let saving = $state(false);
|
||||
let is_editing = $state(false);
|
||||
|
||||
async function load_data() {
|
||||
loading = true;
|
||||
@@ -34,24 +35,6 @@
|
||||
load_data();
|
||||
});
|
||||
|
||||
async function handle_save() {
|
||||
saving = true;
|
||||
const data_kv: any = {};
|
||||
editable_fields__address.forEach(field => {
|
||||
if (address[field] !== undefined) {
|
||||
data_kv[field] = address[field];
|
||||
}
|
||||
});
|
||||
|
||||
await update_ae_obj__address({
|
||||
api_cfg: $ae_api,
|
||||
address_id,
|
||||
data_kv,
|
||||
log_lvl: 1
|
||||
});
|
||||
saving = false;
|
||||
}
|
||||
|
||||
async function handle_delete() {
|
||||
if (!confirm('Permanently delete this address?')) return;
|
||||
await delete_ae_obj_id__address({
|
||||
@@ -76,11 +59,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn variant-soft-error" onclick={handle_delete} disabled={loading || saving}>
|
||||
<Trash2 size={16} class="mr-2" /> Delete
|
||||
<button class="btn btn-sm variant-soft-secondary" onclick={() => is_editing = !is_editing} disabled={loading}>
|
||||
{#if is_editing}
|
||||
<Eye size={16} class="mr-2" /> View Mode
|
||||
{:else}
|
||||
<Edit size={16} class="mr-2" /> Edit Mode
|
||||
{/if}
|
||||
</button>
|
||||
<button class="btn variant-filled-primary" onclick={handle_save} disabled={loading || saving}>
|
||||
<Save size={16} class="mr-2" /> Save Changes
|
||||
<button class="btn btn-sm variant-soft-error" onclick={handle_delete} disabled={loading}>
|
||||
<Trash2 size={16} class="mr-2" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -88,72 +75,99 @@
|
||||
{#if loading}
|
||||
<div class="placeholder animate-pulse w-full h-64"></div>
|
||||
{:else if address}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Address Details</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span>City</span>
|
||||
<input class="input" type="text" bind:value={address.city} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>State / Province</span>
|
||||
<input class="input" type="text" bind:value={address.state_province} />
|
||||
</label>
|
||||
<label class="label md:col-span-2">
|
||||
<span>Country</span>
|
||||
<input class="input" type="text" bind:value={address.country} />
|
||||
</label>
|
||||
{#if is_editing}
|
||||
<Address_form
|
||||
{address}
|
||||
onSave={(updated) => {
|
||||
address = updated;
|
||||
is_editing = false;
|
||||
}}
|
||||
onCancel={() => is_editing = false}
|
||||
/>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Address Details</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Attention To</p>
|
||||
<p>{address.attention_to || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Organization</p>
|
||||
<p>{address.organization_name || '--'}</p>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Address Lines</p>
|
||||
<p>{address.line_1}</p>
|
||||
{#if address.line_2}<p>{address.line_2}</p>{/if}
|
||||
{#if address.line_3}<p>{address.line_3}</p>{/if}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">City, State/Province</p>
|
||||
<p>{address.city}, {address.state_province || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Postal Code / Country</p>
|
||||
<p>{address.postal_code || '--'} / {address.country_name || address.country || '--'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Technical Details</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Timezone</p>
|
||||
<p>{address.timezone || '--'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Coordinates</p>
|
||||
<p>{address.latitude || '--'}, {address.longitude || '--'}</p>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<p class="text-xs opacity-60 uppercase font-bold">Internal Notes</p>
|
||||
<p class="whitespace-pre-wrap">{address.notes || '--'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Internal Metadata</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="label">
|
||||
<span>Group</span>
|
||||
<input class="input" type="text" bind:value={address.group} />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Sort Priority</span>
|
||||
<input class="input" type="number" bind:value={address.sort} />
|
||||
</label>
|
||||
<label class="label md:col-span-2">
|
||||
<span>Internal Notes</span>
|
||||
<textarea class="textarea" rows="3" bind:value={address.notes}></textarea>
|
||||
</label>
|
||||
<div class="space-y-6">
|
||||
<div class="card p-6 space-y-4 variant-soft">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Status</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Enabled</span>
|
||||
<span class="badge {address.enable ? 'variant-filled-success' : 'variant-filled-error'}">
|
||||
{address.enable ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Hidden</span>
|
||||
<span class="badge {address.hide ? 'variant-filled-warning' : 'variant-filled-surface'}">
|
||||
{address.hide ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>Priority</span>
|
||||
<span class="badge {address.priority ? 'variant-filled-secondary' : 'variant-filled-surface'}">
|
||||
{address.priority ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 opacity-60 text-xs font-mono variant-soft">
|
||||
<p>ID: {address.address_id_random}</p>
|
||||
<p>Created: {new Date(address.created_on).toLocaleString()}</p>
|
||||
{#if address.updated_on}
|
||||
<p>Updated: {new Date(address.updated_on).toLocaleString()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="card p-4 space-y-4">
|
||||
<h3 class="h4 border-b border-surface-500/30 pb-2">Status & Visibility</h3>
|
||||
<div class="space-y-4">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={address.enable} />
|
||||
<p>Enabled</p>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={address.hide} />
|
||||
<p>Hidden from Public</p>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={address.priority} />
|
||||
<p>High Priority</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 space-y-2 opacity-60 text-sm font-mono">
|
||||
<p>ID: {address.address_id_random}</p>
|
||||
<p>Created: {new Date(address.created_on).toLocaleString()}</p>
|
||||
{#if address.updated_on}
|
||||
<p>Updated: {new Date(address.updated_on).toLocaleString()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
258
src/routes/core/addresses/ae_comp__address_form.svelte
Normal file
258
src/routes/core/addresses/ae_comp__address_form.svelte
Normal file
@@ -0,0 +1,258 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Address Form Component
|
||||
* Standardized 2026-01-09 for Core UI Polish.
|
||||
* Uses unified ae_Address type and Svelte 5 Runes.
|
||||
*/
|
||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
||||
import { update_ae_obj__address, create_ae_obj__address } from '$lib/ae_core/ae_core__address';
|
||||
import type { ae_Address } from '$lib/types/ae_types';
|
||||
import { Save, X, MapPin, Globe, Clock, Navigation } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
address?: ae_Address | null;
|
||||
onSave?: (address: ae_Address) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
let { address = null, onSave, onCancel }: Props = $props();
|
||||
|
||||
// Form State (Runes)
|
||||
let formData = $state({
|
||||
attention_to: address?.attention_to ?? '',
|
||||
organization_name: address?.organization_name ?? '',
|
||||
line_1: address?.line_1 ?? '',
|
||||
line_2: address?.line_2 ?? '',
|
||||
line_3: address?.line_3 ?? '',
|
||||
city: address?.city ?? '',
|
||||
state_province: address?.state_province ?? '',
|
||||
postal_code: address?.postal_code ?? '',
|
||||
country: address?.country ?? '',
|
||||
// country_name: address?.country_name ?? '', // DO NOT USE - Scott 2026-01-09
|
||||
timezone: address?.timezone ?? '',
|
||||
latitude: address?.latitude ?? '',
|
||||
longitude: address?.longitude ?? '',
|
||||
notes: address?.notes ?? '',
|
||||
enable: address?.enable ?? true,
|
||||
hide: address?.hide ?? false,
|
||||
priority: address?.priority ?? false
|
||||
});
|
||||
|
||||
let is_loading = $state(false);
|
||||
let error_msg = $state('');
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
is_loading = true;
|
||||
error_msg = '';
|
||||
|
||||
// Surgical Payload
|
||||
const payload: any = { ...formData };
|
||||
for (const key in payload) {
|
||||
if (typeof payload[key] === 'string' && payload[key].trim() === '') {
|
||||
// line_1 and city are likely required, but we'll trim them
|
||||
if (key === 'line_1' || key === 'city') {
|
||||
payload[key] = payload[key].trim();
|
||||
} else {
|
||||
payload[key] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (address?.address_id_random) {
|
||||
// Update existing
|
||||
result = await update_ae_obj__address({
|
||||
api_cfg: $ae_api,
|
||||
address_id: address.address_id_random,
|
||||
data_kv: payload,
|
||||
log_lvl: 1
|
||||
});
|
||||
} else {
|
||||
// Create new
|
||||
result = await create_ae_obj__address({
|
||||
api_cfg: $ae_api,
|
||||
account_id: $ae_loc.account_id,
|
||||
data_kv: payload,
|
||||
log_lvl: 1
|
||||
});
|
||||
}
|
||||
|
||||
if (result) {
|
||||
if (onSave) onSave(result);
|
||||
} else {
|
||||
error_msg = 'Failed to save address record.';
|
||||
}
|
||||
} catch (err: any) {
|
||||
error_msg = err.message || 'An error occurred while saving.';
|
||||
} finally {
|
||||
is_loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form onsubmit={handleSubmit} class="card p-6 space-y-6 shadow-xl variant-glass-surface">
|
||||
<header class="flex justify-between items-center border-b border-surface-500/30 pb-4">
|
||||
<h3 class="h3 flex items-center gap-2">
|
||||
<MapPin size={24} />
|
||||
{address ? 'Edit Address' : 'Create New Address'}
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
{#if onCancel}
|
||||
<button type="button" class="btn btn-sm variant-soft" onclick={onCancel}>
|
||||
<X size={16} class="mr-1" /> Cancel
|
||||
</button>
|
||||
{/if}
|
||||
<button type="submit" class="btn btn-sm variant-filled-primary" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{:else}
|
||||
<Save size={16} class="mr-1" />
|
||||
{/if}
|
||||
Save Address
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if error_msg}
|
||||
<aside class="alert variant-filled-error">
|
||||
<div class="alert-message">
|
||||
<p>{error_msg}</p>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Location Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Location Details</legend>
|
||||
|
||||
<label class="label">
|
||||
<span>Attention To / Name</span>
|
||||
<input class="input" type="text" bind:value={formData.attention_to} placeholder="John Doe" />
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Organization Name</span>
|
||||
<input class="input" type="text" bind:value={formData.organization_name} placeholder="Acme Corp" />
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span>Address Line 1</span>
|
||||
<input class="input" type="text" bind:value={formData.line_1} required placeholder="123 Main St" />
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Line 2</span>
|
||||
<input class="input" type="text" bind:value={formData.line_2} placeholder="Suite 100" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Line 3</span>
|
||||
<input class="input" type="text" bind:value={formData.line_3} placeholder="Floor 2" />
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Region Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Region & Code</legend>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>City</span>
|
||||
<input class="input" type="text" bind:value={formData.city} required placeholder="Metropolis" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>State / Province</span>
|
||||
<input class="input" type="text" bind:value={formData.state_province} placeholder="NY" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Postal Code</span>
|
||||
<input class="input" type="text" bind:value={formData.postal_code} placeholder="12345" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Country (Code)</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Globe size={16} /></div>
|
||||
<input type="text" bind:value={formData.country} placeholder="USA" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- DO NOT USE - Scott 2026-01-09 -->
|
||||
<!-- <label class="label">
|
||||
<span>Country Name</span>
|
||||
<input class="input" type="text" bind:value={formData.country_name} placeholder="United States" />
|
||||
</label> -->
|
||||
</fieldset>
|
||||
|
||||
<!-- Technical Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Technical & GIS</legend>
|
||||
|
||||
<label class="label">
|
||||
<span>Timezone</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Clock size={16} /></div>
|
||||
<input type="text" bind:value={formData.timezone} placeholder="America/New_York" />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="label">
|
||||
<span>Latitude</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Navigation size={16} /></div>
|
||||
<input type="text" bind:value={formData.latitude} placeholder="40.7128" />
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Longitude</span>
|
||||
<div class="input-group input-group-divider grid-cols-[auto_1fr]">
|
||||
<div class="input-group-shim"><Navigation size={16} /></div>
|
||||
<input type="text" bind:value={formData.longitude} placeholder="-74.0060" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Status Section -->
|
||||
<fieldset class="space-y-4">
|
||||
<legend class="text-sm font-bold uppercase tracking-widest opacity-60">Status</legend>
|
||||
|
||||
<div class="flex flex-wrap gap-4 pt-2">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.enable} />
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.hide} />
|
||||
<span>Hidden</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input class="checkbox" type="checkbox" bind:checked={formData.priority} />
|
||||
<span>Priority</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label">
|
||||
<span>Internal Notes</span>
|
||||
<textarea class="textarea" rows="2" bind:value={formData.notes} placeholder="Notes about this address..."></textarea>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-end gap-2 border-t border-surface-500/30 pt-4">
|
||||
<button type="submit" class="btn variant-filled-primary w-full md:w-auto" disabled={is_loading}>
|
||||
{#if is_loading}
|
||||
<span class="animate-spin mr-2">⏳</span>
|
||||
{/if}
|
||||
{address ? 'Update Address' : 'Create Address'}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
Reference in New Issue
Block a user