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 +188 -0
- package/index.d.ts +23 -0
- package/index.js +3 -0
- package/package.json +33 -0
- package/src/business-card.js +396 -0
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
|
+

|
|
6
|
+

|
|
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
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
|
+
|