@axium/contacts 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/format.d.ts +1 -8
- package/dist/common.d.ts +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/vcard.d.ts +2 -0
- package/dist/vcard.js +82 -0
- package/lib/ContactPicture.svelte +1 -0
- package/lib/InitForm.svelte +24 -10
- package/locales/en.json +2 -1
- package/package.json +1 -1
- package/routes/contacts/[id]/+page.svelte +26 -7
package/dist/client/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Address, Contact, InitNoExternal, Phone } from '../common.js';
|
|
1
|
+
import type { Address, Contact, InitNoExternal, Phone, SigDateLike } from '../common.js';
|
|
2
2
|
/**
|
|
3
3
|
* Display name for contact
|
|
4
4
|
* @todo localize
|
|
@@ -17,12 +17,5 @@ export declare function phoneDefault(contact: Contact): string;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function address(addr: Address): string;
|
|
19
19
|
export declare function birthDate(contact: Contact): string;
|
|
20
|
-
interface SigDateLike {
|
|
21
|
-
year?: number | null;
|
|
22
|
-
month?: number | null;
|
|
23
|
-
day?: number | null;
|
|
24
|
-
label?: string | null;
|
|
25
|
-
}
|
|
26
20
|
export declare function date(sig: SigDateLike): string;
|
|
27
21
|
export declare function job(contact: InitNoExternal): string;
|
|
28
|
-
export {};
|
package/dist/common.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export declare const SigDate: z.ZodObject<{
|
|
|
35
35
|
}, z.core.$strip>;
|
|
36
36
|
export interface SigDate extends z.infer<typeof SigDate> {
|
|
37
37
|
}
|
|
38
|
+
export interface SigDateLike {
|
|
39
|
+
year?: number | null;
|
|
40
|
+
month?: number | null;
|
|
41
|
+
day?: number | null;
|
|
42
|
+
label?: string | null;
|
|
43
|
+
}
|
|
38
44
|
export declare const Relationship: z.ZodObject<{
|
|
39
45
|
to: z.ZodUUID;
|
|
40
46
|
label: z.ZodString;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/vcard.d.ts
ADDED
package/dist/vcard.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function encodeDate(date) {
|
|
2
|
+
const y = date.year ? String(date.year).padStart(4, '0') : '--';
|
|
3
|
+
const m = date.month ? String(date.month).padStart(2, '0') : '-';
|
|
4
|
+
const d = date.day ? String(date.day).padStart(2, '0') : '';
|
|
5
|
+
return `${y}-${m}-${d}`;
|
|
6
|
+
}
|
|
7
|
+
export function toVCard(contact) {
|
|
8
|
+
const lines = ['BEGIN:VCARD', 'VERSION:3.0'];
|
|
9
|
+
function escape(str) {
|
|
10
|
+
if (!str)
|
|
11
|
+
return '';
|
|
12
|
+
return str.replace(/([,;\\])/g, '\\$1').replace(/\n/g, '\\n');
|
|
13
|
+
}
|
|
14
|
+
const n = [contact.surname, contact.givenName, contact.givenName2, contact.prefix, contact.suffix];
|
|
15
|
+
if (n.some(Boolean)) {
|
|
16
|
+
lines.push(`N:${n.map(escape).join(';')}`);
|
|
17
|
+
}
|
|
18
|
+
if (contact.display) {
|
|
19
|
+
lines.push(`FN:${escape(contact.display)}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const full = [contact.prefix, contact.givenName, contact.givenName2, contact.surname, contact.suffix].filter(Boolean).join(' ');
|
|
23
|
+
lines.push(`FN:${escape(full) || 'Unknown'}`);
|
|
24
|
+
}
|
|
25
|
+
if (contact.nickname)
|
|
26
|
+
lines.push(`NICKNAME:${escape(contact.nickname)}`);
|
|
27
|
+
if (contact.company || contact.department) {
|
|
28
|
+
lines.push(`ORG:${escape(contact.company)};${escape(contact.department)}`);
|
|
29
|
+
}
|
|
30
|
+
if (contact.jobTitle)
|
|
31
|
+
lines.push(`TITLE:${escape(contact.jobTitle)}`);
|
|
32
|
+
if (contact.notes)
|
|
33
|
+
lines.push(`NOTE:${escape(contact.notes)}`);
|
|
34
|
+
if (contact.birthYear || contact.birthMonth || contact.birthDay) {
|
|
35
|
+
lines.push(`BDAY:${encodeDate({ year: contact.birthYear, month: contact.birthMonth, day: contact.birthDay })}`);
|
|
36
|
+
}
|
|
37
|
+
for (const url of contact.urls) {
|
|
38
|
+
lines.push(`URL:${escape(url)}`);
|
|
39
|
+
}
|
|
40
|
+
for (const email of contact.emails) {
|
|
41
|
+
const type = email.label ? `;TYPE=${escape(email.label)}` : '';
|
|
42
|
+
const pref = email.isDefault ? ';TYPE=PREF' : '';
|
|
43
|
+
lines.push(`EMAIL${type}${pref}:${escape(email.email)}`);
|
|
44
|
+
}
|
|
45
|
+
for (const phone of contact.phones) {
|
|
46
|
+
const type = phone.label ? `;TYPE=${escape(phone.label)}` : '';
|
|
47
|
+
const pref = phone.isDefault ? ';TYPE=PREF' : '';
|
|
48
|
+
const country = phone.country ? `+${phone.country}` : '';
|
|
49
|
+
lines.push(`TEL${type}${pref}:${country}${phone.number}`);
|
|
50
|
+
}
|
|
51
|
+
for (const addr of contact.addresses) {
|
|
52
|
+
const type = addr.label ? `;TYPE=${escape(addr.label)}` : '';
|
|
53
|
+
const pref = addr.isDefault ? ';TYPE=PREF' : '';
|
|
54
|
+
const components = [
|
|
55
|
+
'', // PO Box
|
|
56
|
+
addr.street2,
|
|
57
|
+
addr.street1,
|
|
58
|
+
addr.locality,
|
|
59
|
+
addr.subdivision,
|
|
60
|
+
addr.postalCode,
|
|
61
|
+
addr.country,
|
|
62
|
+
];
|
|
63
|
+
lines.push(`ADR${type}${pref}:${components.map(escape).join(';')}`);
|
|
64
|
+
}
|
|
65
|
+
for (const date of contact.dates) {
|
|
66
|
+
const type = date.label ? `;TYPE=${escape(date.label)}` : '';
|
|
67
|
+
lines.push(`ANNIVERSARY${type}:${encodeDate(date)}`);
|
|
68
|
+
}
|
|
69
|
+
for (const rel of contact.relationships) {
|
|
70
|
+
// @todo get name of `to` since other software would just see the UUID
|
|
71
|
+
const type = rel.label ? `;TYPE=${escape(rel.label)}` : '';
|
|
72
|
+
lines.push(`RELATED${type}:${escape(rel.to)}`);
|
|
73
|
+
}
|
|
74
|
+
for (const custom of contact.custom) {
|
|
75
|
+
const safeLabel = custom.label?.replace(/[^a-zA-Z0-9-]/g, '');
|
|
76
|
+
if (!safeLabel)
|
|
77
|
+
continue;
|
|
78
|
+
lines.push(`X-${safeLabel}:${escape(custom.value)}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push('END:VCARD');
|
|
81
|
+
return lines.join('\r\n');
|
|
82
|
+
}
|
package/lib/InitForm.svelte
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import ContactPicture from './ContactPicture.svelte';
|
|
7
7
|
import DateSelect from './DateSelect.svelte';
|
|
8
8
|
import Discovery from './Discovery.svelte';
|
|
9
|
+
import { name as formatName } from '@axium/contacts/client/format';
|
|
9
10
|
|
|
10
11
|
let showDetailed = $state(false);
|
|
11
12
|
|
|
@@ -54,6 +55,8 @@
|
|
|
54
55
|
</button>
|
|
55
56
|
</a>
|
|
56
57
|
|
|
58
|
+
<span></span>
|
|
59
|
+
|
|
57
60
|
<button class="icon-text save" onclick={() => save(init)}>
|
|
58
61
|
<span>{text('contacts.init.save')}</span>
|
|
59
62
|
</button>
|
|
@@ -61,7 +64,8 @@
|
|
|
61
64
|
|
|
62
65
|
{#if init.id}
|
|
63
66
|
<div class="contact-init-header">
|
|
64
|
-
<ContactPicture contact={init as typeof init & { id: string }} --size="
|
|
67
|
+
<ContactPicture contact={init as typeof init & { id: string }} --size="150px" />
|
|
68
|
+
<span class="contact-name-title">{formatName(init)}</span>
|
|
65
69
|
</div>
|
|
66
70
|
{/if}
|
|
67
71
|
|
|
@@ -196,34 +200,44 @@
|
|
|
196
200
|
</div>
|
|
197
201
|
|
|
198
202
|
<style>
|
|
203
|
+
.contact-name-title {
|
|
204
|
+
margin-left: 2em;
|
|
205
|
+
font-size: 2em;
|
|
206
|
+
}
|
|
207
|
+
|
|
199
208
|
.contact-init-header,
|
|
200
209
|
.contact-init,
|
|
201
210
|
.contact-init-actions {
|
|
202
211
|
padding: 2em;
|
|
203
|
-
|
|
212
|
+
margin: 1em;
|
|
213
|
+
width: calc(700px - 2em);
|
|
204
214
|
|
|
205
215
|
@media (width < 700px) {
|
|
206
|
-
width: 100
|
|
216
|
+
width: calc(100% - 2em);
|
|
207
217
|
}
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
.contact-init-header
|
|
211
|
-
display: flex;
|
|
212
|
-
align-items: center;
|
|
213
|
-
justify-content: center;
|
|
214
|
-
}
|
|
215
|
-
|
|
220
|
+
.contact-init-header,
|
|
216
221
|
.contact-init-actions {
|
|
217
222
|
display: flex;
|
|
218
223
|
gap: 1em;
|
|
219
224
|
align-items: center;
|
|
220
|
-
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.contact-init-actions > :not(span) {
|
|
228
|
+
flex: 0 0 auto;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.contact-init-actions > span {
|
|
232
|
+
flex: 1 1 auto;
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
.contact-init {
|
|
224
236
|
display: grid;
|
|
225
237
|
grid-template-columns: 1em 1fr 1em;
|
|
226
238
|
gap: 1em;
|
|
239
|
+
border-radius: 1em;
|
|
240
|
+
background-color: var(--bg-menu);
|
|
227
241
|
|
|
228
242
|
button.toggle {
|
|
229
243
|
height: 1em;
|
package/locales/en.json
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { text } from '@axium/client';
|
|
3
|
-
import { Icon, Popover } from '@axium/client/components';
|
|
2
|
+
import { fetchAPI, text } from '@axium/client';
|
|
3
|
+
import { FormDialog, Icon, Popover } from '@axium/client/components';
|
|
4
4
|
import { toast, toastStatus } from '@axium/client/toast';
|
|
5
|
+
import { toVCard } from '@axium/contacts';
|
|
5
6
|
import { format, getContact } from '@axium/contacts/client';
|
|
6
7
|
import { ContactPicture, Field } from '@axium/contacts/components';
|
|
7
|
-
import { upload } from 'utilium/dom.js';
|
|
8
|
+
import { download, upload } from 'utilium/dom.js';
|
|
8
9
|
|
|
9
10
|
const { data } = $props();
|
|
10
11
|
const { contact } = data;
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
await toast('error', e);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
const contactFileName = contact.emails.find(e => e.isDefault)?.email || format.name(contact).replaceAll(' ', '_');
|
|
35
38
|
</script>
|
|
36
39
|
|
|
37
40
|
<svelte:head>
|
|
@@ -55,12 +58,12 @@
|
|
|
55
58
|
</button>
|
|
56
59
|
</a>
|
|
57
60
|
|
|
58
|
-
<button class="icon-text">
|
|
61
|
+
<button class="icon-text" command="show-modal" commandfor="delete-contact">
|
|
59
62
|
<Icon i="trash" />
|
|
60
63
|
<span>{text('contacts.delete')}</span>
|
|
61
64
|
</button>
|
|
62
65
|
|
|
63
|
-
<button class="icon-text">
|
|
66
|
+
<button class="icon-text" onclick={() => download(contactFileName + '.vcard', toVCard(contact))}>
|
|
64
67
|
<Icon i="file-export" />
|
|
65
68
|
<span>{text('contacts.export')}</span>
|
|
66
69
|
</button>
|
|
@@ -176,6 +179,19 @@
|
|
|
176
179
|
{@render part('regular/note', contact.notes)}
|
|
177
180
|
</div>
|
|
178
181
|
|
|
182
|
+
<FormDialog
|
|
183
|
+
id="delete-contact"
|
|
184
|
+
submitDanger
|
|
185
|
+
submitText={text('generic.delete')}
|
|
186
|
+
submit={() => fetchAPI('DELETE', 'contacts/:id', {}, contact.id).then(() => (window.location.href = '/contacts'))}
|
|
187
|
+
>
|
|
188
|
+
<p>
|
|
189
|
+
<span>{text('contacts.delete_confirm')}</span>
|
|
190
|
+
<br />
|
|
191
|
+
<strong>{text('generic.action_irreversible')}</strong>
|
|
192
|
+
</p>
|
|
193
|
+
</FormDialog>
|
|
194
|
+
|
|
179
195
|
<style>
|
|
180
196
|
.contact-image-container {
|
|
181
197
|
width: 150px;
|
|
@@ -201,10 +217,11 @@
|
|
|
201
217
|
.contact,
|
|
202
218
|
.contact-actions {
|
|
203
219
|
padding: 2em;
|
|
204
|
-
|
|
220
|
+
margin: 1em;
|
|
221
|
+
width: calc(700px - 2em);
|
|
205
222
|
|
|
206
223
|
@media (width < 700px) {
|
|
207
|
-
width: 100
|
|
224
|
+
width: calc(100% - 2em);
|
|
208
225
|
}
|
|
209
226
|
}
|
|
210
227
|
|
|
@@ -227,6 +244,8 @@
|
|
|
227
244
|
display: grid;
|
|
228
245
|
grid-template-columns: 1em 1fr;
|
|
229
246
|
gap: 1em;
|
|
247
|
+
border-radius: 1em;
|
|
248
|
+
background-color: var(--bg-menu);
|
|
230
249
|
|
|
231
250
|
button.toggle {
|
|
232
251
|
height: 1em;
|