@ansiversa/components 0.0.126 → 0.0.127
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/index.ts +2 -0
- package/package.json +1 -1
- package/src/components/media/AvImageUploader.astro +341 -0
- package/src/media/types.ts +13 -0
- package/src/templates/portfolio-public/Classic.astro +7 -0
- package/src/templates/portfolio-public/Gallery.astro +7 -0
- package/src/templates/portfolio-public/Minimal.astro +7 -0
- package/src/templates/portfolio-public/Story.astro +7 -0
- package/src/templates/portfolio-public/types.ts +1 -0
package/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export { default as QuizSummary } from './src/Summary/QuizSummary.astro';
|
|
|
37
37
|
export { default as FlashNoteSummary } from './src/Summary/FlashNoteSummary.astro';
|
|
38
38
|
export { default as ResumeBuilderSummary } from './src/Summary/ResumeBuilderSummary.astro';
|
|
39
39
|
export { default as PortfolioCreatorSummary } from './src/Summary/PortfolioCreatorSummary.astro';
|
|
40
|
+
export { default as AvImageUploader } from "./src/components/media/AvImageUploader.astro";
|
|
40
41
|
export { default as ResumeBuilderShell } from './src/resume-templates/ResumeBuilderShell.astro';
|
|
41
42
|
export { default as ResumeTemplateClassic } from './src/resume-templates/ResumeTemplateClassic.astro';
|
|
42
43
|
export { default as ResumeTemplateModernTwoTone } from './src/resume-templates/ResumeTemplateModernTwoTone.astro';
|
|
@@ -58,6 +59,7 @@ export type {
|
|
|
58
59
|
PortfolioPublicTemplateKey,
|
|
59
60
|
PortfolioPublicSectionKey,
|
|
60
61
|
} from "./src/templates/portfolio-public";
|
|
62
|
+
export type { AvMediaUploadResponse, AvMediaUploadResult } from "./src/media/types";
|
|
61
63
|
|
|
62
64
|
export * from "./src/alpine";
|
|
63
65
|
export * from "./src/Summary/types";
|
package/package.json
CHANGED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
uploadUrl: string;
|
|
4
|
+
scope: "account" | "portfolio-profile";
|
|
5
|
+
purpose?: "avatar";
|
|
6
|
+
portfolioId?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
helpText?: string;
|
|
9
|
+
initialUrl?: string | null;
|
|
10
|
+
maxBytes?: number;
|
|
11
|
+
onUploadedEventName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
uploadUrl,
|
|
16
|
+
scope,
|
|
17
|
+
purpose = "avatar",
|
|
18
|
+
portfolioId = "",
|
|
19
|
+
label = "Profile photo",
|
|
20
|
+
helpText = "JPG, PNG, or WebP up to 2 MB.",
|
|
21
|
+
initialUrl = null,
|
|
22
|
+
maxBytes = 2 * 1024 * 1024,
|
|
23
|
+
onUploadedEventName = "av:uploaded",
|
|
24
|
+
} = Astro.props as Props;
|
|
25
|
+
|
|
26
|
+
if (!uploadUrl) {
|
|
27
|
+
throw new Error("AvImageUploader requires an `uploadUrl` prop.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const uploaderId = `av-image-uploader-${Math.random().toString(36).slice(2, 10)}`;
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<div class="av-image-uploader" id={uploaderId}>
|
|
34
|
+
<p class="av-image-uploader__label">{label}</p>
|
|
35
|
+
{helpText ? <p class="av-image-uploader__help">{helpText}</p> : null}
|
|
36
|
+
|
|
37
|
+
<div class="av-image-uploader__row">
|
|
38
|
+
<div class="av-image-uploader__preview-wrap">
|
|
39
|
+
<img
|
|
40
|
+
class={`av-image-uploader__preview ${initialUrl ? "is-visible" : ""}`}
|
|
41
|
+
src={initialUrl ?? ""}
|
|
42
|
+
alt="Uploaded image preview"
|
|
43
|
+
data-av-preview
|
|
44
|
+
/>
|
|
45
|
+
<div
|
|
46
|
+
class={`av-image-uploader__placeholder ${initialUrl ? "is-hidden" : ""}`}
|
|
47
|
+
data-av-placeholder
|
|
48
|
+
>
|
|
49
|
+
No photo
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="av-image-uploader__controls">
|
|
54
|
+
<label class="av-image-uploader__button" data-av-trigger for={`${uploaderId}-input`}>
|
|
55
|
+
Choose image
|
|
56
|
+
</label>
|
|
57
|
+
<input
|
|
58
|
+
id={`${uploaderId}-input`}
|
|
59
|
+
class="av-image-uploader__input"
|
|
60
|
+
type="file"
|
|
61
|
+
accept="image/jpeg,image/png,image/webp"
|
|
62
|
+
data-av-input
|
|
63
|
+
/>
|
|
64
|
+
<p class="av-image-uploader__status" data-av-status></p>
|
|
65
|
+
<p class="av-image-uploader__error" data-av-error role="status"></p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<script
|
|
71
|
+
define:vars={{
|
|
72
|
+
uploaderId,
|
|
73
|
+
uploadUrl,
|
|
74
|
+
scope,
|
|
75
|
+
purpose,
|
|
76
|
+
portfolioId,
|
|
77
|
+
maxBytes,
|
|
78
|
+
onUploadedEventName,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
(() => {
|
|
82
|
+
const root = document.getElementById(uploaderId);
|
|
83
|
+
if (!root) return;
|
|
84
|
+
|
|
85
|
+
const input = root.querySelector("[data-av-input]");
|
|
86
|
+
const trigger = root.querySelector("[data-av-trigger]");
|
|
87
|
+
const preview = root.querySelector("[data-av-preview]");
|
|
88
|
+
const placeholder = root.querySelector("[data-av-placeholder]");
|
|
89
|
+
const status = root.querySelector("[data-av-status]");
|
|
90
|
+
const error = root.querySelector("[data-av-error]");
|
|
91
|
+
|
|
92
|
+
const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
93
|
+
|
|
94
|
+
const formatBytes = (bytes) => {
|
|
95
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
96
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
97
|
+
const kb = bytes / 1024;
|
|
98
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
99
|
+
return `${(kb / 1024).toFixed(1)} MB`;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const setUploading = (isUploading) => {
|
|
103
|
+
if (trigger) {
|
|
104
|
+
trigger.textContent = isUploading ? "Uploading..." : "Choose image";
|
|
105
|
+
trigger.classList.toggle("is-disabled", isUploading);
|
|
106
|
+
}
|
|
107
|
+
if (input) {
|
|
108
|
+
input.disabled = isUploading;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const setError = (message) => {
|
|
113
|
+
if (error) {
|
|
114
|
+
error.textContent = message ?? "";
|
|
115
|
+
}
|
|
116
|
+
if (status && message) {
|
|
117
|
+
status.textContent = "";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const setStatus = (message) => {
|
|
122
|
+
if (status) {
|
|
123
|
+
status.textContent = message ?? "";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const setPreview = (url) => {
|
|
128
|
+
if (!preview || !placeholder) return;
|
|
129
|
+
|
|
130
|
+
if (url) {
|
|
131
|
+
preview.src = url;
|
|
132
|
+
preview.classList.add("is-visible");
|
|
133
|
+
placeholder.classList.add("is-hidden");
|
|
134
|
+
} else {
|
|
135
|
+
preview.src = "";
|
|
136
|
+
preview.classList.remove("is-visible");
|
|
137
|
+
placeholder.classList.remove("is-hidden");
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const parseError = async (res) => {
|
|
142
|
+
try {
|
|
143
|
+
const body = await res.json();
|
|
144
|
+
return body?.error || body?.message || "Upload failed.";
|
|
145
|
+
} catch {
|
|
146
|
+
return "Upload failed.";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
input?.addEventListener("change", async (event) => {
|
|
151
|
+
const target = event.currentTarget;
|
|
152
|
+
const file = target?.files?.[0];
|
|
153
|
+
if (!file) return;
|
|
154
|
+
|
|
155
|
+
setError("");
|
|
156
|
+
setStatus("");
|
|
157
|
+
|
|
158
|
+
if (!allowedTypes.has(file.type)) {
|
|
159
|
+
setError("Please upload a JPG, PNG, or WebP image.");
|
|
160
|
+
target.value = "";
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (file.size > maxBytes) {
|
|
165
|
+
setError(`Image must be ${formatBytes(maxBytes)} or smaller.`);
|
|
166
|
+
target.value = "";
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setUploading(true);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const form = new FormData();
|
|
174
|
+
form.set("purpose", purpose);
|
|
175
|
+
form.set("scope", scope);
|
|
176
|
+
if (scope === "portfolio-profile" && portfolioId) {
|
|
177
|
+
form.set("portfolioId", portfolioId);
|
|
178
|
+
}
|
|
179
|
+
form.set("file", file);
|
|
180
|
+
|
|
181
|
+
const res = await fetch(uploadUrl, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
body: form,
|
|
184
|
+
credentials: "include",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
throw new Error(await parseError(res));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const payload = await res.json();
|
|
192
|
+
if (!payload?.ok || !payload?.media?.url) {
|
|
193
|
+
throw new Error("Upload response is invalid.");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const media = payload.media;
|
|
197
|
+
setPreview(media.url);
|
|
198
|
+
setStatus(`Uploaded ${formatBytes(media.size)} (${media.width}x${media.height})`);
|
|
199
|
+
setError("");
|
|
200
|
+
|
|
201
|
+
root.dispatchEvent(
|
|
202
|
+
new CustomEvent(onUploadedEventName, {
|
|
203
|
+
bubbles: true,
|
|
204
|
+
detail: media,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
} catch (uploadError) {
|
|
208
|
+
setError(uploadError?.message || "Upload failed.");
|
|
209
|
+
} finally {
|
|
210
|
+
setUploading(false);
|
|
211
|
+
target.value = "";
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
})();
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<style>
|
|
218
|
+
.av-image-uploader {
|
|
219
|
+
display: grid;
|
|
220
|
+
gap: 0.6rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.av-image-uploader__label {
|
|
224
|
+
margin: 0;
|
|
225
|
+
font-size: 0.9rem;
|
|
226
|
+
font-weight: 600;
|
|
227
|
+
color: #e2e8f0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.av-image-uploader__help {
|
|
231
|
+
margin: 0;
|
|
232
|
+
font-size: 0.8rem;
|
|
233
|
+
color: #94a3b8;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.av-image-uploader__row {
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-wrap: wrap;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 1rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.av-image-uploader__preview-wrap {
|
|
244
|
+
width: 86px;
|
|
245
|
+
height: 86px;
|
|
246
|
+
position: relative;
|
|
247
|
+
flex: 0 0 auto;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.av-image-uploader__preview,
|
|
251
|
+
.av-image-uploader__placeholder {
|
|
252
|
+
width: 86px;
|
|
253
|
+
height: 86px;
|
|
254
|
+
border-radius: 999px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.av-image-uploader__preview {
|
|
258
|
+
display: none;
|
|
259
|
+
object-fit: cover;
|
|
260
|
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
261
|
+
background: rgba(15, 23, 42, 0.6);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.av-image-uploader__preview.is-visible {
|
|
265
|
+
display: block;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.av-image-uploader__placeholder {
|
|
269
|
+
display: inline-flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: center;
|
|
272
|
+
border: 1px dashed rgba(148, 163, 184, 0.45);
|
|
273
|
+
background: rgba(15, 23, 42, 0.45);
|
|
274
|
+
color: #94a3b8;
|
|
275
|
+
font-size: 0.72rem;
|
|
276
|
+
text-transform: uppercase;
|
|
277
|
+
letter-spacing: 0.06em;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.av-image-uploader__placeholder.is-hidden {
|
|
281
|
+
display: none;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.av-image-uploader__controls {
|
|
285
|
+
display: grid;
|
|
286
|
+
gap: 0.35rem;
|
|
287
|
+
min-width: 220px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.av-image-uploader__button {
|
|
291
|
+
display: inline-flex;
|
|
292
|
+
align-items: center;
|
|
293
|
+
justify-content: center;
|
|
294
|
+
width: fit-content;
|
|
295
|
+
min-width: 130px;
|
|
296
|
+
padding: 0.52rem 0.9rem;
|
|
297
|
+
border-radius: 0.65rem;
|
|
298
|
+
border: 1px solid rgba(100, 116, 139, 0.4);
|
|
299
|
+
background: rgba(15, 23, 42, 0.75);
|
|
300
|
+
color: #e2e8f0;
|
|
301
|
+
font-size: 0.8rem;
|
|
302
|
+
font-weight: 600;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
transition: border-color 120ms ease, background 120ms ease;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.av-image-uploader__button:hover {
|
|
308
|
+
border-color: rgba(148, 163, 184, 0.65);
|
|
309
|
+
background: rgba(30, 41, 59, 0.85);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.av-image-uploader__button.is-disabled {
|
|
313
|
+
opacity: 0.7;
|
|
314
|
+
cursor: progress;
|
|
315
|
+
pointer-events: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.av-image-uploader__input {
|
|
319
|
+
position: absolute;
|
|
320
|
+
width: 1px;
|
|
321
|
+
height: 1px;
|
|
322
|
+
padding: 0;
|
|
323
|
+
margin: -1px;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
clip: rect(0, 0, 0, 0);
|
|
326
|
+
border: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.av-image-uploader__status {
|
|
330
|
+
margin: 0;
|
|
331
|
+
color: #93c5fd;
|
|
332
|
+
font-size: 0.78rem;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.av-image-uploader__error {
|
|
336
|
+
margin: 0;
|
|
337
|
+
min-height: 1em;
|
|
338
|
+
color: #fca5a5;
|
|
339
|
+
font-size: 0.78rem;
|
|
340
|
+
}
|
|
341
|
+
</style>
|
|
@@ -22,6 +22,13 @@ const hasContact =
|
|
|
22
22
|
<main class="mx-auto grid w-full max-w-6xl gap-10 px-4 py-10 lg:grid-cols-[280px_1fr] lg:px-8">
|
|
23
23
|
<aside class="space-y-8 border-b border-slate-200 pb-8 lg:border-b-0 lg:border-r lg:pb-0 lg:pr-8">
|
|
24
24
|
<header class="space-y-2">
|
|
25
|
+
{data.owner.profilePhotoUrl ? (
|
|
26
|
+
<img
|
|
27
|
+
src={data.owner.profilePhotoUrl}
|
|
28
|
+
alt={`${ownerName} profile photo`}
|
|
29
|
+
class="h-16 w-16 rounded-full border border-slate-200 object-cover"
|
|
30
|
+
/>
|
|
31
|
+
) : null}
|
|
25
32
|
<h1 class="text-3xl font-bold tracking-tight text-slate-950">{ownerName}</h1>
|
|
26
33
|
{data.owner.headline ? <p class="text-base text-slate-700">{data.owner.headline}</p> : null}
|
|
27
34
|
{data.owner.location ? <p class="text-sm text-slate-500">{data.owner.location}</p> : null}
|
|
@@ -14,6 +14,13 @@ const ownerName = data.owner.fullName || data.meta?.title || "Portfolio";
|
|
|
14
14
|
<main class="mx-auto w-full max-w-6xl px-4 py-10 lg:px-8">
|
|
15
15
|
<header class="mb-8 rounded-2xl border border-slate-200 bg-slate-50 p-6 sm:p-8">
|
|
16
16
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">Portfolio</p>
|
|
17
|
+
{data.owner.profilePhotoUrl ? (
|
|
18
|
+
<img
|
|
19
|
+
src={data.owner.profilePhotoUrl}
|
|
20
|
+
alt={`${ownerName} profile photo`}
|
|
21
|
+
class="mt-4 h-16 w-16 rounded-full border border-slate-200 object-cover"
|
|
22
|
+
/>
|
|
23
|
+
) : null}
|
|
17
24
|
<h1 class="mt-3 text-3xl font-bold tracking-tight text-slate-950 sm:text-4xl">{ownerName}</h1>
|
|
18
25
|
{data.owner.headline ? <p class="mt-2 text-lg text-slate-700">{data.owner.headline}</p> : null}
|
|
19
26
|
{hasText(data.owner.summary) ? <p class="mt-4 max-w-3xl leading-7 text-slate-700">{data.owner.summary}</p> : null}
|
|
@@ -13,6 +13,13 @@ const ownerName = data.owner.fullName || data.meta?.title || "Portfolio";
|
|
|
13
13
|
<article class="min-h-screen bg-white text-slate-900">
|
|
14
14
|
<main class="mx-auto w-full max-w-3xl px-6 py-12 sm:px-8">
|
|
15
15
|
<header class="border-b border-slate-200 pb-8">
|
|
16
|
+
{data.owner.profilePhotoUrl ? (
|
|
17
|
+
<img
|
|
18
|
+
src={data.owner.profilePhotoUrl}
|
|
19
|
+
alt={`${ownerName} profile photo`}
|
|
20
|
+
class="mb-4 h-16 w-16 rounded-full border border-slate-200 object-cover"
|
|
21
|
+
/>
|
|
22
|
+
) : null}
|
|
16
23
|
<h1 class="text-4xl font-light tracking-tight text-slate-950 sm:text-5xl">{ownerName}</h1>
|
|
17
24
|
{data.owner.headline ? <p class="mt-2 text-lg text-slate-700">{data.owner.headline}</p> : null}
|
|
18
25
|
{data.owner.location ? <p class="mt-1 text-sm uppercase tracking-[0.16em] text-slate-500">{data.owner.location}</p> : null}
|
|
@@ -14,6 +14,13 @@ const ownerName = data.owner.fullName || data.meta?.title || "Portfolio";
|
|
|
14
14
|
<main class="mx-auto w-full max-w-5xl px-5 py-12 lg:px-10">
|
|
15
15
|
<header class="mx-auto max-w-3xl space-y-4 text-center">
|
|
16
16
|
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">Story Portfolio</p>
|
|
17
|
+
{data.owner.profilePhotoUrl ? (
|
|
18
|
+
<img
|
|
19
|
+
src={data.owner.profilePhotoUrl}
|
|
20
|
+
alt={`${ownerName} profile photo`}
|
|
21
|
+
class="mx-auto h-20 w-20 rounded-full border border-slate-200 object-cover"
|
|
22
|
+
/>
|
|
23
|
+
) : null}
|
|
17
24
|
<h1 class="text-4xl font-bold tracking-tight text-slate-950 sm:text-5xl">{ownerName}</h1>
|
|
18
25
|
{data.owner.headline ? <p class="text-lg text-slate-700">{data.owner.headline}</p> : null}
|
|
19
26
|
{hasText(data.owner.summary) ? <p class="leading-7 text-slate-700">{data.owner.summary}</p> : null}
|