solid-vcard-business-card 1.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.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # vCard Business Card for Solid
2
+
3
+ A lightweight, elegant web component that renders a beautiful business card from a [Solid Pod](https://solidproject.org/) vCard profile. This component automatically fetches and displays profile information stored in your Solid Pod using the vCard vocabulary.
4
+
5
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
6
+ ![npm](https://img.shields.io/npm/v/vcard-business-card-solid)
7
+
8
+ ## Features
9
+
10
+ - 🎨 **Elegant Design** - Premium business card styling with animated golden gradients
11
+ - 🔒 **Solid Pod Integration** - Fetches profile data directly from Solid Pods
12
+ - 📇 **vCard Standard** - Uses the standard vCard vocabulary
13
+
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install vcard-business-card-solid
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### In a JavaScript Module
24
+
25
+ ```javascript
26
+ import 'vcard-business-card-solid';
27
+ ```
28
+
29
+ Then use the component in your HTML:
30
+
31
+ ```html
32
+ <vcard-business-card src="https://your-pod.solidcommunity.net/profile/card#me"></vcard-business-card>
33
+ ```
34
+
35
+ ### In HTML (with bundler)
36
+
37
+ ```html
38
+ <!DOCTYPE html>
39
+ <html>
40
+ <head>
41
+ <title>My Business Card</title>
42
+ </head>
43
+ <body>
44
+ <vcard-business-card src="https://your-pod.solidcommunity.net/profile/card#me"></vcard-business-card>
45
+
46
+ <script type="module">
47
+ import 'vcard-business-card-solid';
48
+ </script>
49
+ </body>
50
+ </html>
51
+ ```
52
+
53
+ ### Vue 3 Example
54
+
55
+ **main.js or main.ts:**
56
+ ```javascript
57
+ import { createApp } from 'vue';
58
+ import App from './App.vue';
59
+ import 'vcard-business-card-solid';
60
+
61
+ const app = createApp(App);
62
+
63
+ // Configure Vue to recognize the custom element
64
+ app.config.compilerOptions.isCustomElement = (tag) => tag === 'vcard-business-card';
65
+
66
+ app.mount('#app');
67
+ ```
68
+
69
+ **Component:**
70
+ ```vue
71
+ <script setup>
72
+ import { ref } from 'vue';
73
+
74
+ const profileUrl = ref('https://your-pod.solidcommunity.net/profile/card#me');
75
+ </script>
76
+
77
+ <template>
78
+ <vcard-business-card :src="profileUrl"></vcard-business-card>
79
+ </template>
80
+ ```
81
+
82
+ ### React Example
83
+
84
+ **index.js or main.tsx:**
85
+ ```javascript
86
+ import React from 'react';
87
+ import ReactDOM from 'react-dom/client';
88
+ import App from './App';
89
+ import 'vcard-business-card-solid'; // Register the web component
90
+
91
+ const root = ReactDOM.createRoot(document.getElementById('root'));
92
+ root.render(
93
+ <React.StrictMode>
94
+ <App />
95
+ </React.StrictMode>
96
+ );
97
+ ```
98
+
99
+ **App.jsx:**
100
+ ```jsx
101
+
102
+ function App() {
103
+ return (
104
+ <vcard-business-card
105
+ src="https://your-pod.solidcommunity.net/profile/card#me"
106
+ />
107
+ );
108
+ }
109
+
110
+ export default App;
111
+ ```
112
+
113
+ ## Profile Requirements
114
+
115
+ Your Solid Pod profile must use the vCard vocabulary with the following fields:
116
+
117
+ ### Required Fields
118
+
119
+ - **Full Name** (`vcard:fn`) - Your full name
120
+
121
+ ### Optional Fields
122
+
123
+ - **Email** (`vcard:hasEmail`) - Your email address
124
+ - **Birthday** (`vcard:bday`) - Your date of birth
125
+ - **Photo** (`vcard:hasPhoto`) - URL to your profile picture
126
+
127
+ ### Example Profile Structure
128
+
129
+ ```turtle
130
+ PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
131
+ PREFIX rdfa: <http://www.w3.org/ns/rdfa#>
132
+
133
+ <https://id.inrupt.com/example> a vcard:Individual;
134
+ vcard:hasEmail <mailto:example@example.com>;
135
+ vcard:fn "Example User";
136
+ vcard:bday "17-03-2001";
137
+ vcard:hasPhoto <image url> .
138
+
139
+ ```
140
+
141
+ **Note:** Ensure your profile has public read permissions so the component can fetch it.
142
+
143
+
144
+ ## Browser Support
145
+
146
+ This component uses native Web Components (Custom Elements) and is supported in:
147
+ - Chrome/Edge 67+
148
+ - Firefox 63+
149
+ - Safari 10.1+
150
+ - Opera 54+
151
+
152
+ ## Error Handling
153
+
154
+ The component displays user-friendly error messages if:
155
+ - The profile URL is invalid or unreachable
156
+ - The profile doesn't contain vCard data
157
+ - Network issues occur
158
+
159
+ ## Privacy & Security
160
+
161
+ - The component only **reads** public profile data
162
+ - No authentication is required
163
+ - No data is stored or transmitted to third parties
164
+ - All communication is direct between the browser and the Solid Pod
165
+
166
+ ## Dependencies
167
+
168
+ - [`@inrupt/solid-client`](https://www.npmjs.com/package/@inrupt/solid-client) - Solid Pod data operations
169
+ - [`@inrupt/vocab-common-rdf`](https://www.npmjs.com/package/@inrupt/vocab-common-rdf) - RDF vocabularies including vCard
170
+
171
+
172
+ ## Contributing
173
+
174
+ Contributions are welcome! Please feel free to submit a Pull Request.
175
+
176
+ ## Links
177
+
178
+ - [Solid Project](https://solidproject.org/)
179
+ - [vCard Ontology](http://www.w3.org/2006/vcard/ns)
180
+
181
+ ## Changelog
182
+
183
+ See [CHANGELOG.md](CHANGELOG.md) for version history.
184
+
185
+ ---
186
+
187
+ Made with ❤️ for the Solid community
188
+
package/index.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export interface SolidProfile {
2
+ name: string;
3
+ email: string;
4
+ birthday: string;
5
+ photoUrl: string | null;
6
+ }
7
+
8
+ export class SolidBusinessCard extends HTMLElement {
9
+ static get observedAttributes(): string[];
10
+
11
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
12
+ connectedCallback(): Promise<void>;
13
+ disconnectedCallback(): void;
14
+
15
+ fetchProfile(profileUrl: string): Promise<void>;
16
+ }
17
+
18
+ declare global {
19
+ interface HTMLElementTagNameMap {
20
+ 'vcard-business-card': SolidBusinessCard;
21
+ }
22
+ }
23
+
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import {SolidBusinessCard} from './src/business-card.js';
2
+ export {SolidBusinessCard};
3
+
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "solid-vcard-business-card",
3
+ "description": "Native Web component that renders a Solid vCard profile",
4
+ "author": "Jelle Fauconnier",
5
+ "type": "module",
6
+ "version": "1.0.1",
7
+ "main": "index.js",
8
+ "types": "index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./index.js",
12
+ "types": "./index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "src",
17
+ "index.js",
18
+ "index.d.ts",
19
+ "README.md"
20
+ ],
21
+ "sideEffects": "True",
22
+ "keywords": [
23
+ "solid",
24
+ "vcard",
25
+ "web-component",
26
+ "business-card"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@inrupt/solid-client": "^1.30.2",
31
+ "@inrupt/vocab-common-rdf": "^1.0.5"
32
+ }
33
+ }
@@ -0,0 +1,396 @@
1
+ import {
2
+ getSolidDataset,
3
+ getThingAll,
4
+ getStringNoLocale,
5
+ getUrl,
6
+ } from "@inrupt/solid-client";
7
+ import { VCARD } from "@inrupt/vocab-common-rdf";
8
+
9
+ export class SolidBusinessCard extends HTMLElement {
10
+ constructor() {
11
+ super();
12
+ this._profile = null;
13
+ }
14
+
15
+ static get observedAttributes() {
16
+ return ['src'];
17
+ }
18
+
19
+ attributeChangedCallback(name, oldValue, newValue) {
20
+ if (name === 'src' && oldValue !== newValue) {
21
+ this.fetchProfile(newValue);
22
+ }
23
+ }
24
+
25
+ async connectedCallback() {
26
+ this.renderLoading();
27
+
28
+ const src = this.getAttribute('src');
29
+ if (src) {
30
+ await this.fetchProfile(src);
31
+ }
32
+
33
+ // Start periodic gradient animation
34
+ this.startGradientAnimation();
35
+ }
36
+
37
+ disconnectedCallback() {
38
+ if (this._animationInterval) {
39
+ clearInterval(this._animationInterval);
40
+ }
41
+ }
42
+
43
+ startGradientAnimation() {
44
+ // Run animation every 10 seconds
45
+ this._animationInterval = setInterval(() => {
46
+ this.animateGradient();
47
+ }, 10000);
48
+
49
+ // Run initial animation after component loads
50
+ setTimeout(() => this.animateGradient(), 500);
51
+ }
52
+
53
+ animateGradient() {
54
+ const nameElement = this.querySelector('.business-card__name');
55
+ if (!nameElement) return;
56
+
57
+ let position = -200;
58
+ const duration = 1000; // 2 seconds for the shimmer to pass
59
+ const steps = 60;
60
+ const stepDuration = duration / steps;
61
+ const positionIncrement = 200 / steps; // Move from -100 to 100
62
+
63
+ const animate = () => {
64
+ if (position >= 200) {
65
+ nameElement.style.backgroundPosition = '-200% 0%';
66
+ return;
67
+ }
68
+
69
+ nameElement.style.backgroundPosition = `${position}% 0%`;
70
+ position += positionIncrement;
71
+
72
+ setTimeout(animate, stepDuration);
73
+ };
74
+
75
+ animate();
76
+ }
77
+
78
+ async fetchProfile(profileUrl) {
79
+ if (!profileUrl) return;
80
+
81
+ try {
82
+ const dataset = await getSolidDataset(profileUrl.trim());
83
+ const things = getThingAll(dataset);
84
+
85
+ let profileThing = null;
86
+ for (const thing of things) {
87
+ const name = getStringNoLocale(thing, VCARD.fn);
88
+ if (name) {
89
+ profileThing = thing;
90
+ break;
91
+ }
92
+ }
93
+
94
+ if (!profileThing) {
95
+ throw new Error("No profile found in dataset");
96
+ }
97
+
98
+
99
+ const name = getStringNoLocale(profileThing, VCARD.fn) || "Unknown Name";
100
+
101
+ const emailUrl = getUrl(profileThing, VCARD.hasEmail);
102
+
103
+ const email = emailUrl ? emailUrl.replace("mailto:", "") : "";
104
+
105
+ const birthday = getStringNoLocale(profileThing, VCARD.bday) || "";
106
+
107
+ const photoUrl = getUrl(profileThing, VCARD.hasPhoto);
108
+
109
+ this._profile = {
110
+ name,
111
+ email,
112
+ birthday,
113
+ photoUrl
114
+ };
115
+
116
+ this.render();
117
+
118
+ } catch (error) {
119
+ console.error("Error loading Solid profile:", error);
120
+ this.renderError(error);
121
+ }
122
+ }
123
+
124
+
125
+ renderLoading() {
126
+ this.innerHTML = `
127
+ <style>
128
+ .business-card {
129
+ font-family: 'Georgia', 'Times New Roman', serif;
130
+ background:
131
+ repeating-linear-gradient(0deg, rgba(0,0,0,0.02) 0px, transparent 1px, transparent 2px, rgba(0,0,0,0.02) 3px),
132
+ repeating-linear-gradient(90deg, rgba(0,0,0,0.02) 0px, transparent 1px, transparent 2px, rgba(0,0,0,0.02) 3px),
133
+ #f5f5f0;
134
+ border-radius: 0.25rem;
135
+ box-shadow:
136
+ 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.05),
137
+ 0 0.25rem 1rem rgba(0, 0, 0, 0.08),
138
+ inset 0 0.0625rem 0.125rem rgba(255, 255, 255, 0.8);
139
+ padding: 2rem;
140
+ width: 21.875rem;
141
+ height: 12.5rem;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ margin: 0.625rem;
146
+ border: 0.0625rem solid rgba(255, 255, 255, 0.5);
147
+ }
148
+ .business-card__loading {
149
+ color: #8a8a82;
150
+ font-size: 0.875rem;
151
+ font-weight: 400;
152
+ letter-spacing: 0.0625rem;
153
+ }
154
+ </style>
155
+ <div class="business-card">
156
+ <div class="business-card__loading">Loading profile...</div>
157
+ </div>
158
+ `;
159
+ }
160
+
161
+ render() {
162
+ if (!this._profile) return;
163
+
164
+ const {name, email, birthday, photoUrl} = this._profile;
165
+
166
+ this.innerHTML = `
167
+ <style>
168
+ .business-card {
169
+ font-family: 'Georgia', 'Times New Roman', serif;
170
+ background:
171
+ linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(0,0,0,0.02) 100%),
172
+ repeating-linear-gradient(0deg, rgba(0,0,0,0.02) 0px, transparent 1px, transparent 2px, rgba(0,0,0,0.02) 3px),
173
+ repeating-linear-gradient(90deg, rgba(0,0,0,0.02) 0px, transparent 1px, transparent 2px, rgba(0,0,0,0.02) 3px),
174
+ #f5f5f0;
175
+ border-radius: 0.25rem;
176
+ box-shadow:
177
+ 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.05),
178
+ 0 0.25rem 1rem rgba(0, 0, 0, 0.08),
179
+ inset 0 0.0625rem 0.125rem rgba(255, 255, 255, 0.8);
180
+ padding: 2.5rem;
181
+ width: 21.875rem;
182
+ height: 12.5rem;
183
+ position: relative;
184
+ overflow: hidden;
185
+ transition: all 0.3s ease;
186
+ margin: 0.625rem;
187
+ border: 0.0625rem solid rgba(255, 255, 255, 0.5);
188
+ }
189
+
190
+ .business-card:hover {
191
+ transform: translateY(-0.1875rem) rotateX(2deg);
192
+ box-shadow:
193
+ 0 0.125rem 0.25rem rgba(0, 0, 0, 0.06),
194
+ 0 0.5rem 1.5rem rgba(0, 0, 0, 0.12),
195
+ inset 0 0.0625rem 0.125rem rgba(255, 255, 255, 0.8);
196
+ }
197
+
198
+ .business-card::before {
199
+ content: '';
200
+ position: absolute;
201
+ top: 0;
202
+ left: 0;
203
+ right: 0;
204
+ bottom: 0;
205
+ background-image:
206
+ radial-gradient(circle at 20% 30%, rgba(0,0,0,0.01) 0%, transparent 50%),
207
+ radial-gradient(circle at 80% 70%, rgba(0,0,0,0.01) 0%, transparent 50%);
208
+ pointer-events: none;
209
+ }
210
+
211
+ .business-card__content {
212
+ position: relative;
213
+ z-index: 1;
214
+ display: flex;
215
+ flex-direction: column;
216
+ height: 100%;
217
+ justify-content: space-between;
218
+ }
219
+
220
+ .business-card__header {
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: 0.75rem;
224
+ padding-bottom: 1rem;
225
+ border-bottom: 0.0625rem solid rgba(200, 200, 190, 0.3);
226
+ }
227
+
228
+ .business-card__name {
229
+ font-size: 2rem;
230
+ font-weight: 400;
231
+ font-family: 'Brush Script MT', cursive;
232
+ background: linear-gradient(90deg,
233
+ #aa8b2e 0%,
234
+ #d4af37 35%,
235
+ #ffecbe 50%,
236
+ #d4af37 65%,
237
+ #aa8b2e 100%);
238
+ -webkit-background-clip: text;
239
+ -webkit-text-fill-color: transparent;
240
+ background-clip: text;
241
+ background-size: 200% 100%;
242
+ background-position: -200% 0%;
243
+ margin: 0;
244
+ letter-spacing: 0.0625rem;
245
+ text-shadow:
246
+ 0.0625rem 0.0625rem 0.125rem rgba(212, 175, 55, 0.3),
247
+ -0.0625rem -0.0625rem 0.125rem rgba(255, 255, 255, 0.5);
248
+ filter: drop-shadow(0 0.0625rem 0.0625rem rgba(0, 0, 0, 0.1));
249
+ position: relative;
250
+ }
251
+
252
+ .business-card__name::before {
253
+ content: '${name}';
254
+ position: absolute;
255
+ top: 0.0625rem;
256
+ left: 0.0625rem;
257
+ z-index: -1;
258
+ opacity: 0.3;
259
+ filter: blur(0.125rem);
260
+ }
261
+
262
+ .business-card__divider {
263
+ width: 3.125rem;
264
+ height: 0.0625rem;
265
+ background: linear-gradient(90deg, #d4af37 0%, transparent 100%);
266
+ margin: 0.5rem 0;
267
+ }
268
+
269
+ .business-card__picture {
270
+ position: absolute;
271
+ top: 1.5rem;
272
+ right: 1.5rem;
273
+ width: 5rem;
274
+ height: 5rem;
275
+ border-radius: 50%;
276
+ overflow: hidden;
277
+ border: 0.125rem solid rgba(212, 175, 55, 0.4);
278
+ box-shadow:
279
+ 0 0.125rem 0.5rem rgba(0, 0, 0, 0.15),
280
+ inset 0 0 0.25rem rgba(255, 255, 255, 0.3);
281
+ }
282
+
283
+ .business-card__picture img {
284
+ width: 100%;
285
+ height: 100%;
286
+ object-fit: cover;
287
+ }
288
+
289
+ .business-card__footer {
290
+ display: flex;
291
+ flex-direction: column;
292
+ gap: 0.375rem;
293
+ }
294
+
295
+ .business-card__contact {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 0.5rem;
299
+ color: #5a5a52;
300
+ font-size: 0.75rem;
301
+ text-decoration: none;
302
+ transition: color 0.2s;
303
+ font-family: 'Segoe UI', sans-serif;
304
+ letter-spacing: 0.03125rem;
305
+ }
306
+
307
+ .business-card__contact:hover {
308
+ color: #d4af37;
309
+ }
310
+
311
+ .business-card__label {
312
+ font-size: 0.625rem;
313
+ color: #8a8a82;
314
+ text-transform: uppercase;
315
+ letter-spacing: 0.0625rem;
316
+ margin-bottom: 0.125rem;
317
+ }
318
+ </style>
319
+ <div class="business-card">
320
+ <div class="business-card__content">
321
+ <div class="business-card__header">
322
+ <h3 class="business-card__name">${name}</h3>
323
+ <div class="business-card__divider"></div>
324
+ </div>
325
+
326
+ <div class="business-card__picture">
327
+ <img src="${photoUrl}" alt="Profile picture">
328
+ </div>
329
+
330
+ <div class="business-card__footer">
331
+ ${email ? `
332
+ <div>
333
+ <div class="business-card__label">Email</div>
334
+ <a class="business-card__contact" href="mailto:${email}">${email}</a>
335
+ </div>
336
+ ` : ''}
337
+ ${birthday ? `
338
+ <div>
339
+ <div class="business-card__label">Birthday</div>
340
+ <div>${birthday}</div>
341
+ </div>
342
+ `: ''}
343
+ </div>
344
+ </div>
345
+ </div>
346
+ `;
347
+ }
348
+
349
+ renderError(error) {
350
+ this.innerHTML = `
351
+ <style>
352
+ .business-card {
353
+ font-family: 'Georgia', 'Times New Roman', serif;
354
+ background:
355
+ repeating-linear-gradient(0deg, rgba(0,0,0,0.02) 0px, transparent 1px, transparent 2px, rgba(0,0,0,0.02) 3px),
356
+ repeating-linear-gradient(90deg, rgba(0,0,0,0.02) 0px, transparent 1px, transparent 2px, rgba(0,0,0,0.02) 3px),
357
+ #fef2f2;
358
+ border-radius: 0.25rem;
359
+ box-shadow:
360
+ 0 0.0625rem 0.125rem rgba(0, 0, 0, 0.05),
361
+ 0 0.25rem 1rem rgba(0, 0, 0, 0.08);
362
+ padding: 2rem;
363
+ width: 21.875rem;
364
+ height: 12.5rem;
365
+ display: flex;
366
+ flex-direction: column;
367
+ align-items: center;
368
+ justify-content: center;
369
+ margin: 0.625rem;
370
+ border: 0.0625rem solid #fecaca;
371
+ }
372
+ .business-card__error-title {
373
+ color: #991b1b;
374
+ font-weight: 600;
375
+ margin: 0 0 0.5rem 0;
376
+ text-align: center;
377
+ font-size: 1rem;
378
+ letter-spacing: 0.03125rem;
379
+ }
380
+ .business-card__error-msg {
381
+ color: #b91c1c;
382
+ font-size: 0.75rem;
383
+ text-align: center;
384
+ line-height: 1.4;
385
+ }
386
+ </style>
387
+ <div class="business-card">
388
+ <h4 class="business-card__error-title">Error Loading Profile</h4>
389
+ <p class="business-card__error-msg">${error.message}</p>
390
+ </div>
391
+ `;
392
+ }
393
+ }
394
+
395
+ customElements.define('vcard-business-card', SolidBusinessCard);
396
+