@axium/contacts 0.0.1

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.
Files changed (45) hide show
  1. package/LICENSE.md +157 -0
  2. package/README.md +3 -0
  3. package/db.json +131 -0
  4. package/dist/client/api.d.ts +2 -0
  5. package/dist/client/api.js +4 -0
  6. package/dist/client/format.d.ts +28 -0
  7. package/dist/client/format.js +71 -0
  8. package/dist/client/index.d.ts +2 -0
  9. package/dist/client/index.js +2 -0
  10. package/dist/client/web_hook.d.ts +8 -0
  11. package/dist/client/web_hook.js +4 -0
  12. package/dist/common.d.ts +698 -0
  13. package/dist/common.js +114 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -0
  16. package/dist/server/api.d.ts +1 -0
  17. package/dist/server/api.js +138 -0
  18. package/dist/server/db.d.ts +21 -0
  19. package/dist/server/db.js +75 -0
  20. package/dist/server/hooks.d.ts +4 -0
  21. package/dist/server/hooks.js +8 -0
  22. package/dist/server/index.d.ts +1 -0
  23. package/dist/server/index.js +1 -0
  24. package/dist/server/pfp.d.ts +1 -0
  25. package/dist/server/pfp.js +81 -0
  26. package/lib/ContactCard.svelte +21 -0
  27. package/lib/ContactPicture.svelte +46 -0
  28. package/lib/DateSelect.svelte +25 -0
  29. package/lib/Discovery.svelte +101 -0
  30. package/lib/Field.svelte +31 -0
  31. package/lib/InitForm.svelte +264 -0
  32. package/lib/List.svelte +37 -0
  33. package/lib/index.ts +7 -0
  34. package/lib/tsconfig.json +13 -0
  35. package/locales/en.json +72 -0
  36. package/package.json +69 -0
  37. package/routes/contacts/+layout.ts +10 -0
  38. package/routes/contacts/+page.svelte +29 -0
  39. package/routes/contacts/+page.ts +15 -0
  40. package/routes/contacts/[id]/+layout.ts +12 -0
  41. package/routes/contacts/[id]/+page.svelte +247 -0
  42. package/routes/contacts/[id]/edit/+page.svelte +25 -0
  43. package/routes/contacts/new/+page.svelte +33 -0
  44. package/routes/contacts/new/+page.ts +11 -0
  45. package/routes/tsconfig.json +13 -0
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ text: string;
4
+ isDefault?: boolean;
5
+ label?: string | null;
6
+ link?: string;
7
+ }
8
+
9
+ const { text, isDefault, label, link }: Props = $props();
10
+ </script>
11
+
12
+ {#snippet content()}
13
+ {#each text.split('\n') as line, i}
14
+ {#if i}<br />{/if}
15
+ <span>{line}</span>
16
+ {/each}
17
+ {/snippet}
18
+
19
+ <span>
20
+ {#if link}
21
+ <a href={link} target="_blank" rel="noopener noreferrer">{@render content()}</a>
22
+ {:else}
23
+ {@render content()}
24
+ {/if}
25
+ {#if label}
26
+ <span class="subtle"> • {label}</span>
27
+ {/if}
28
+ {#if isDefault}
29
+ <span class="subtle"> (default)</span>
30
+ {/if}
31
+ </span>
@@ -0,0 +1,264 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { dynamicRows } from '@axium/client/attachments';
4
+ import { Icon, LocationSelect, ZodInput } from '@axium/client/components';
5
+ import { ContactURL, Custom, Email, Init, Phone, Relationship, SigDate, type Contact } from '@axium/contacts';
6
+ import ContactPicture from './ContactPicture.svelte';
7
+ import DateSelect from './DateSelect.svelte';
8
+ import Discovery from './Discovery.svelte';
9
+
10
+ let showDetailed = $state(false);
11
+
12
+ let {
13
+ init = $bindable<Init & Partial<Contact>>(),
14
+ save,
15
+ back = '/contacts',
16
+ }: { init: Init & Partial<Contact>; save(init: Init): unknown; back?: string } = $props();
17
+
18
+ let more = $state({
19
+ names: false,
20
+ job: false,
21
+ });
22
+
23
+ type ArrayField = 'emails' | 'phones' | 'addresses' | 'relationships' | 'urls' | 'dates' | 'custom';
24
+
25
+ function updateValue() {}
26
+ </script>
27
+
28
+ {#snippet zod(name: keyof Init, showMore?: boolean, detailed?: boolean)}
29
+ {#if !detailed || showMore || init[name]}
30
+ <ZodInput bind:rootValue={init} path={name} schema={Init.shape[name]} {updateValue} noLabel="placeholder" />
31
+ {/if}
32
+ {/snippet}
33
+
34
+ {#snippet moreToggle(key: keyof typeof more)}
35
+ <button class="reset icon-text toggle" onclick={() => (more[key] = !more[key])}>
36
+ <Icon i="chevron-{more[key] ? 'up' : 'down'}" />
37
+ </button>
38
+ {/snippet}
39
+
40
+ {#snippet add(field: ArrayField)}
41
+ {#if init[field].length}<span />{/if}
42
+ <button class="icon-text" onclick={() => init[field].push({} as any)}>
43
+ <Icon i="plus" />
44
+ <span>{text('contacts.init.add.' + field)}</span>
45
+ </button>
46
+ <span />
47
+ {/snippet}
48
+
49
+ <div class="contact-init-actions">
50
+ <a href={back}>
51
+ <button class="icon-text">
52
+ <Icon i="arrow-left" />
53
+ <span>{text('contacts.back')}</span>
54
+ </button>
55
+ </a>
56
+
57
+ <button class="icon-text save" onclick={() => save(init)}>
58
+ <span>{text('contacts.init.save')}</span>
59
+ </button>
60
+ </div>
61
+
62
+ {#if init.id}
63
+ <div class="contact-init-header">
64
+ <ContactPicture contact={init as typeof init & { id: string }} --size="100px" />
65
+ </div>
66
+ {/if}
67
+
68
+ <div class="contact-init">
69
+ <Icon i="user" />
70
+ <div class="section">
71
+ {@render zod('display', more.names, true)}
72
+ {@render zod('prefix', more.names, true)}
73
+ {@render zod('givenName', more.names)}
74
+ {@render zod('givenName2', more.names, true)}
75
+ {@render zod('surname', more.names)}
76
+ {@render zod('suffix', more.names, true)}
77
+ {@render zod('nickname', more.names, true)}
78
+ </div>
79
+ {@render moreToggle('names')}
80
+
81
+ <Icon i="regular/buildings" />
82
+ <div class="section">
83
+ {@render zod('company', more.job)}
84
+ {@render zod('jobTitle', more.job)}
85
+ {@render zod('department', more.job, true)}
86
+ </div>
87
+ {@render moreToggle('job')}
88
+
89
+ <Icon i="regular/envelope" />
90
+ {#each init.emails, i}
91
+ {#if i}<span />{/if}
92
+ <div class={['email', init.emails.length > 1 && 'with-label']}>
93
+ <ZodInput bind:rootValue={init} path="emails.{i}.email" schema={Email.shape.email} {updateValue} noLabel="placeholder" />
94
+ {#if init.emails.length > 1}
95
+ <ZodInput bind:rootValue={init} path="emails.{i}.label" schema={Email.shape.label} {updateValue} noLabel="placeholder" />
96
+ {/if}
97
+ </div>
98
+ <Icon i="xmark" onclick={() => init.emails.splice(i, 1)} />
99
+ {/each}
100
+ {@render add('emails')}
101
+
102
+ <Icon i="phone" />
103
+ {#each init.phones, i}
104
+ {#if i}<span />{/if}
105
+ <div class={['phone', init.phones.length > 1 && 'with-label']}>
106
+ <ZodInput bind:rootValue={init} path="phones.{i}.country" schema={Phone.shape.country} {updateValue} noLabel="placeholder" />
107
+ <ZodInput bind:rootValue={init} path="phones.{i}.number" schema={Phone.shape.number} {updateValue} noLabel="placeholder" />
108
+ {#if init.phones.length > 1}
109
+ <ZodInput bind:rootValue={init} path="phones.{i}.label" schema={Phone.shape.label} {updateValue} noLabel="placeholder" />
110
+ {/if}
111
+ </div>
112
+ <Icon i="xmark" onclick={() => init.phones.splice(i, 1)} />
113
+ {/each}
114
+ {@render add('phones')}
115
+
116
+ <Icon i="regular/location-dot" />
117
+ {#each init.addresses, i}
118
+ {#if i}<span />{/if}
119
+ <div class="section">
120
+ <LocationSelect bind:value={init.addresses[i]} />
121
+ </div>
122
+ <Icon i="xmark" onclick={() => init.addresses.splice(i, 1)} />
123
+ {/each}
124
+
125
+ {@render add('addresses')}
126
+
127
+ <Icon i="cake-candles" />
128
+ <div class="section">
129
+ <DateSelect bind:day={init.birthDay} bind:month={init.birthMonth} bind:year={init.birthYear} />
130
+ </div>
131
+ <span />
132
+
133
+ {#if showDetailed}
134
+ <Icon i="regular/circle-nodes" />
135
+ {#each init.relationships as rel, i}
136
+ {#if i}<span />{/if}
137
+ <div class="section">
138
+ <Discovery
139
+ exclude={[init.id, ...init.relationships.map(r => r.to)].filter((v): v is string => !!v)}
140
+ onSelect={id => (rel.to = id)}
141
+ />
142
+ <ZodInput
143
+ bind:rootValue={init}
144
+ path="relationships.{i}.label"
145
+ schema={Relationship.shape.label}
146
+ {updateValue}
147
+ noLabel="placeholder"
148
+ />
149
+ </div>
150
+ <Icon i="xmark" onclick={() => init.relationships.splice(i, 1)} />
151
+ {/each}
152
+ {@render add('relationships')}
153
+
154
+ <Icon i="regular/calendar-day" />
155
+ {#each init.dates, i}
156
+ {#if i}<span />{/if}
157
+ <div class="section">
158
+ <ZodInput bind:rootValue={init} path="dates.{i}" schema={SigDate} {updateValue} noLabel="placeholder" />
159
+ </div>
160
+ <Icon i="xmark" onclick={() => init.dates.splice(i, 1)} />
161
+ {/each}
162
+ {@render add('dates')}
163
+
164
+ <Icon i="link-simple" />
165
+ {#each init.urls, i}
166
+ {#if i}<span />{/if}
167
+ <div class="section">
168
+ <ZodInput bind:rootValue={init} path="urls.{i}" schema={ContactURL} {updateValue} noLabel="placeholder" />
169
+ </div>
170
+ <Icon i="xmark" onclick={() => init.urls.splice(i, 1)} />
171
+ {/each}
172
+ {@render add('urls')}
173
+
174
+ <Icon i="regular/input-text" />
175
+ {#each init.custom, i}
176
+ {#if i}<span />{/if}
177
+ <div class="section">
178
+ <ZodInput bind:rootValue={init} path="custom.{i}" schema={Custom} {updateValue} noLabel="placeholder" />
179
+ </div>
180
+ <Icon i="xmark" onclick={() => init.custom.splice(i, 1)} />
181
+ {/each}
182
+ {@render add('custom')}
183
+ {/if}
184
+
185
+ <Icon i="regular/note" />
186
+ <textarea bind:value={init.notes} {@attach dynamicRows(50, 3)}></textarea>
187
+ <span />
188
+
189
+ <span />
190
+ <div>
191
+ <button onclick={() => (showDetailed = !showDetailed)}>
192
+ <span>{text('contacts.init.show_' + (showDetailed ? 'less' : 'more'))}</span>
193
+ </button>
194
+ </div>
195
+ <span />
196
+ </div>
197
+
198
+ <style>
199
+ .contact-init-header,
200
+ .contact-init,
201
+ .contact-init-actions {
202
+ padding: 2em;
203
+ width: 700px;
204
+
205
+ @media (width < 700px) {
206
+ width: 100%;
207
+ }
208
+ }
209
+
210
+ .contact-init-header {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ }
215
+
216
+ .contact-init-actions {
217
+ display: flex;
218
+ gap: 1em;
219
+ align-items: center;
220
+ justify-content: space-between;
221
+ }
222
+
223
+ .contact-init {
224
+ display: grid;
225
+ grid-template-columns: 1em 1fr 1em;
226
+ gap: 1em;
227
+
228
+ button.toggle {
229
+ height: 1em;
230
+ }
231
+
232
+ :global(.ZodInput) {
233
+ display: flex;
234
+ flex-direction: column;
235
+ gap: 1em;
236
+ }
237
+ }
238
+
239
+ .section {
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: 0.25em;
243
+ }
244
+
245
+ .email {
246
+ display: grid;
247
+ gap: 0.5em;
248
+ grid-template-columns: 1fr;
249
+
250
+ &.with-label {
251
+ grid-template-columns: 1fr 1fr;
252
+ }
253
+ }
254
+
255
+ .phone {
256
+ display: grid;
257
+ gap: 0.5em;
258
+ grid-template-columns: 4em 1fr;
259
+
260
+ &.with-label {
261
+ grid-template-columns: 4em 1fr 1fr;
262
+ }
263
+ }
264
+ </style>
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import '@axium/client/styles/list';
4
+ import type { Contact } from '@axium/contacts';
5
+ import { format } from '@axium/contacts/client';
6
+ import ContactPicture from './ContactPicture.svelte';
7
+
8
+ const { contacts }: { contacts: Contact[] } = $props();
9
+ </script>
10
+
11
+ <div class="list">
12
+ <div class="list-item list-header">
13
+ <span></span>
14
+ <span>{text('contacts.list.name')}</span>
15
+ <span>{text('contacts.list.email')}</span>
16
+ <span>{text('contacts.list.phone')}</span>
17
+ <span>{text('contacts.list.job')}</span>
18
+ </div>
19
+
20
+ {#each contacts as contact (contact.id)}
21
+ <div class="list-item" onclick={() => (location.href = `/contacts/${contact.id}`)}>
22
+ <ContactPicture {contact} />
23
+ <span>{format.name(contact)}</span>
24
+ <span>{format.emailDefault(contact)}</span>
25
+ <span>{format.phoneDefault(contact)}</span>
26
+ <span>{format.job(contact)}</span>
27
+ </div>
28
+ {:else}
29
+ <p class="list-empty">{text('contacts.list.empty')}</p>
30
+ {/each}
31
+ </div>
32
+
33
+ <style>
34
+ .list-item {
35
+ grid-template-columns: 2em 2fr minmax(10em, 25em) minmax(10em, 20em) 1fr;
36
+ }
37
+ </style>
package/lib/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { default as ContactCard } from './ContactCard.svelte';
2
+ export { default as ContactPicture } from './ContactPicture.svelte';
3
+ export { default as DateSelect } from './DateSelect.svelte';
4
+ export { default as Discovery } from './Discovery.svelte';
5
+ export { default as Field } from './Field.svelte';
6
+ export { default as InitForm } from './InitForm.svelte';
7
+ export { default as List } from './List.svelte';
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "../.svelte-kit/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "noEmit": true,
6
+ "module": "preserve",
7
+ "moduleResolution": "Bundler",
8
+ "types": ["@sveltejs/kit"]
9
+ },
10
+ "include": ["**/*.svelte", "**/*.ts"],
11
+ "exclude": [],
12
+ "references": [{ "path": ".." }]
13
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "app_name": {
3
+ "contacts": "Contacts"
4
+ },
5
+ "contacts": {
6
+ "back": "Back",
7
+ "edit": "Edit",
8
+ "edit_title": "Contacts — Edit",
9
+ "delete": "Delete",
10
+ "export": "Export",
11
+ "new_button": "New Contact",
12
+ "field_loading": "Loading...",
13
+ "named_title": "Contacts — {name}",
14
+ "list": {
15
+ "name": "Name",
16
+ "email": "Email",
17
+ "phone": "Phone number",
18
+ "job": "Job title and company",
19
+ "empty": "No contacts"
20
+ },
21
+ "init": {
22
+ "title": "Contacts — New",
23
+ "save": "Save",
24
+ "add": {
25
+ "emails": "Add email",
26
+ "phones": "Add phone number",
27
+ "addresses": "Add address",
28
+ "relationships": "Add relationship",
29
+ "dates": "Add significant date",
30
+ "urls": "Add website",
31
+ "custom": "Add custom field"
32
+ },
33
+ "show_more": "Show more",
34
+ "show_less": "Show less"
35
+ },
36
+ "image": {
37
+ "update": "Upload",
38
+ "remove": "Remove",
39
+ "toast_updated": "Updated contact picture",
40
+ "toast_removed": "Removed contact picture"
41
+ },
42
+ "Discovery": {
43
+ "placeholder": "Search for a contact",
44
+ "no_results": "No contacts found"
45
+ }
46
+ },
47
+ "contact": {
48
+ "label": "Label",
49
+ "default": "Default",
50
+ "email": "Email",
51
+ "phone": {
52
+ "number": "Number"
53
+ },
54
+ "relationship": {
55
+ "to": "To"
56
+ },
57
+ "custom": {
58
+ "value": "Value"
59
+ },
60
+ "display": "Display as",
61
+ "prefix": "Prefix",
62
+ "given_name": "First name",
63
+ "given_name_2": "Middle name",
64
+ "surname": "Last name",
65
+ "suffix": "Suffix",
66
+ "nickname": "Nickname",
67
+ "company": "Company",
68
+ "job_title": "Job title",
69
+ "department": "Department",
70
+ "notes": "Notes"
71
+ }
72
+ }
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@axium/contacts",
3
+ "version": "0.0.1",
4
+ "author": "James Prevett <axium@jamespre.dev>",
5
+ "description": "Contacts for Axium",
6
+ "funding": {
7
+ "type": "individual",
8
+ "url": "https://github.com/sponsors/james-pre"
9
+ },
10
+ "license": "LGPL-3.0-or-later",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/james-pre/axium.git"
14
+ },
15
+ "homepage": "https://github.com/james-pre/axium#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/james-pre/axium/issues"
18
+ },
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": "./dist/index.js",
24
+ "./client": "./dist/client/index.js",
25
+ "./server": "./dist/server/index.js",
26
+ "./*": "./dist/*.js",
27
+ "./components": "./lib/index.js",
28
+ "./components/*": "./lib/*.svelte"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "lib",
33
+ "routes",
34
+ "locales",
35
+ "db.json"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc"
39
+ },
40
+ "peerDependencies": {
41
+ "@axium/client": ">=0.22.0",
42
+ "@axium/core": ">=0.26.0",
43
+ "@axium/server": ">=0.42.0",
44
+ "@sveltejs/kit": "^2.27.3",
45
+ "utilium": "^2.4.0"
46
+ },
47
+ "dependencies": {
48
+ "zod": "^4.0.5"
49
+ },
50
+ "axium": {
51
+ "server": {
52
+ "routes": "routes",
53
+ "hooks": "./dist/server/hooks.js",
54
+ "db": "./db.json",
55
+ "web_client_hooks": "./dist/client/web_hook.js"
56
+ },
57
+ "apps": [
58
+ {
59
+ "id": "contacts",
60
+ "name": "Contacts",
61
+ "icon": "address-book"
62
+ }
63
+ ],
64
+ "update_checks": true,
65
+ "config": {
66
+ "auto_link": true
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,10 @@
1
+ import { getCurrentSession } from '@axium/client/user';
2
+ import type { Session, UserPublic } from '@axium/core';
3
+
4
+ export async function load({ parent }) {
5
+ let { session }: { session?: (Session & { user: UserPublic }) | null } = await parent();
6
+
7
+ session ||= await getCurrentSession().catch(() => null);
8
+
9
+ return { session };
10
+ }
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { Icon } from '@axium/client/components';
4
+ import { List as ContactList } from '@axium/contacts/components';
5
+
6
+ const { data } = $props();
7
+ </script>
8
+
9
+ <svelte:head>
10
+ <title>{text('app_name.contacts')}</title>
11
+ </svelte:head>
12
+
13
+ <div class="contact-list-container">
14
+ <div>
15
+ <a href="/contacts/new">
16
+ <button class="icon-text">
17
+ <Icon i="plus" />
18
+ <span>{text('contacts.new_button')}</span>
19
+ </button>
20
+ </a>
21
+ </div>
22
+ <ContactList contacts={data.contacts} />
23
+ </div>
24
+
25
+ <style>
26
+ .contact-list-container {
27
+ margin: 2em;
28
+ }
29
+ </style>
@@ -0,0 +1,15 @@
1
+ import { fetchAPI } from '@axium/client/requests';
2
+ import type {} from '@axium/contacts/common';
3
+ import { redirect } from '@sveltejs/kit';
4
+
5
+ export const ssr = false;
6
+
7
+ export async function load({ parent }) {
8
+ let { session } = await parent();
9
+
10
+ if (!session) redirect(307, '/login?after=/contacts');
11
+
12
+ const contacts = await fetchAPI('GET', 'users/:id/contacts', {}, session.userId);
13
+
14
+ return { contacts, session };
15
+ }
@@ -0,0 +1,12 @@
1
+ import type { Contact } from '@axium/contacts';
2
+ import { getContact } from '@axium/contacts/client';
3
+
4
+ export const ssr = false;
5
+
6
+ export async function load({ parent, params }) {
7
+ let { session } = await parent();
8
+
9
+ const contact: Contact = await getContact(params.id);
10
+
11
+ return { session, contact };
12
+ }