negodesign 0.0.35 → 0.0.37
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/components/core/card/product/01/CardProduct01.svelte +130 -0
- package/dist/components/core/card/product/01/CardProduct01.svelte.d.ts +4 -0
- package/dist/components/core/card/product/02/CardProduct02.svelte +118 -0
- package/dist/components/core/card/product/02/CardProduct02.svelte.d.ts +4 -0
- package/dist/components/core/card/product/CardProduct.svelte +22 -0
- package/dist/components/core/card/product/CardProduct.svelte.d.ts +11 -0
- package/dist/components/core/card/product/data.d.ts +3 -0
- package/dist/components/core/card/product/data.js +89 -0
- package/dist/components/core/card/profile/01/CardProfile01.svelte +0 -1
- package/dist/components/core/card/promotion/CardPromotion.svelte +1 -1
- package/dist/components/core/card/promotion/CardPromotionPrice.svelte.d.ts +8 -0
- package/dist/components/core/card/shared/CardPhoneOrWhatsapp.svelte +12 -5
- package/dist/components/core/card/shared/CardTags.svelte +49 -24
- package/dist/components/core/card/shared/CardTitlePrice.svelte +24 -0
- package/dist/components/core/card/shared/CardTitlePrice.svelte.d.ts +15 -0
- package/dist/components/core/card/shared/TruncatableText.svelte +1 -1
- package/dist/components/core/card/types.d.ts +48 -0
- package/dist/components/core/carousel/grid/media/ui/CarouselGridMedia.svelte +38 -28
- package/dist/components/core/carousel/grid/product/types.d.ts +22 -0
- package/dist/components/core/carousel/grid/product/types.js +1 -0
- package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte +71 -0
- package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte.d.ts +4 -0
- package/dist/components/core/carousel/grid/profile/ui/CarouselGridProfile.svelte +56 -46
- package/dist/components/core/carousel/grid/promotion/types.d.ts +23 -0
- package/dist/components/core/carousel/grid/promotion/types.js +1 -0
- package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte +68 -0
- package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte.d.ts +4 -0
- package/dist/components/core/carousel/highlights/ui/CarouselHighlights.svelte +57 -47
- package/dist/components/core/carousel/product/types.d.ts +21 -0
- package/dist/components/core/carousel/product/types.js +1 -0
- package/dist/components/core/carousel/product/ui/CarouselProduct.svelte +57 -0
- package/dist/components/core/carousel/product/ui/CarouselProduct.svelte.d.ts +4 -0
- package/dist/components/core/carousel/promotion/ui/CarouselPromotion.svelte +57 -47
- package/dist/components/core/panel/NotFoundEmpty.svelte +76 -0
- package/dist/components/core/panel/NotFoundEmpty.svelte.d.ts +16 -0
- package/dist/components/pages/company-profile/CompanyProfile.svelte +144 -0
- package/dist/components/pages/company-profile/CompanyProfile.svelte.d.ts +4 -0
- package/dist/components/pages/company-profile/data.d.ts +2 -0
- package/dist/components/pages/company-profile/data.js +115 -0
- package/dist/components/pages/company-profile/shared/ProfileAboutCompany.svelte +79 -0
- package/dist/components/pages/company-profile/shared/ProfileAboutCompany.svelte.d.ts +9 -0
- package/dist/components/pages/company-profile/shared/ProfileBanner.svelte +270 -0
- package/dist/components/pages/company-profile/shared/ProfileBanner.svelte.d.ts +14 -0
- package/dist/components/pages/company-profile/shared/ProfileGallery.svelte +198 -0
- package/dist/components/pages/company-profile/shared/ProfileGallery.svelte.d.ts +9 -0
- package/dist/components/pages/company-profile/shared/ProfileMenuBadge.svelte +56 -0
- package/dist/components/pages/company-profile/shared/ProfileMenuBadge.svelte.d.ts +11 -0
- package/dist/components/pages/company-profile/shared/ProfileProducts.svelte +62 -0
- package/dist/components/pages/company-profile/shared/ProfileProducts.svelte.d.ts +9 -0
- package/dist/components/pages/company-profile/shared/ProfileSidebar.svelte +57 -0
- package/dist/components/pages/company-profile/shared/ProfileSidebar.svelte.d.ts +11 -0
- package/dist/components/pages/company-profile/types.d.ts +137 -0
- package/dist/components/pages/company-profile/types.js +1 -0
- package/dist/components/pages/privacy-policy-or-terms-of-use/data.d.ts +3 -0
- package/dist/components/pages/privacy-policy-or-terms-of-use/data.js +179 -0
- package/dist/components/pages/privacy-policy-or-terms-of-use/shared/DocSectionView.svelte +2 -1
- package/dist/components/pages/privacy-policy-or-terms-of-use/shared/DocSectionView.svelte.d.ts +1 -0
- package/dist/components/pages/product-details/01/ProductDetails01.svelte +19 -7
- package/dist/components/pages/product-details/02/ProductDetails02.svelte +42 -37
- package/dist/components/pages/product-details/03/ProductDetails03.svelte +33 -29
- package/dist/components/pages/product-details/ProductDetails.svelte +11 -7
- package/dist/components/pages/product-details/shared/ProductDetailFeatures.svelte +11 -5
- package/dist/components/pages/product-details/shared/ProductDetailFeatures.svelte.d.ts +2 -1
- package/dist/components/pages/product-details/shared/ProductDetailFilesVertical.svelte +52 -20
- package/dist/components/pages/product-details/shared/ProductDetailFilesVertical.svelte.d.ts +1 -1
- package/dist/components/pages/product-details/shared/ProductDetailImage.svelte +51 -20
- package/dist/components/pages/product-details/shared/ProductDetailImage.svelte.d.ts +1 -1
- package/dist/components/pages/product-details/shared/ProductDetailPrice.svelte +1 -1
- package/dist/components/pages/product-details/types.d.ts +9 -2
- package/dist/components/pages/profile-user/types.d.ts +7 -0
- package/dist/components/ui/empty/empty-content.svelte +23 -0
- package/dist/components/ui/empty/empty-content.svelte.d.ts +5 -0
- package/dist/components/ui/empty/empty-description.svelte +23 -0
- package/dist/components/ui/empty/empty-description.svelte.d.ts +5 -0
- package/dist/components/ui/empty/empty-header.svelte +20 -0
- package/dist/components/ui/empty/empty-header.svelte.d.ts +5 -0
- package/dist/components/ui/empty/empty-media.svelte +41 -0
- package/dist/components/ui/empty/empty-media.svelte.d.ts +26 -0
- package/dist/components/ui/empty/empty-title.svelte +20 -0
- package/dist/components/ui/empty/empty-title.svelte.d.ts +5 -0
- package/dist/components/ui/empty/empty.svelte +23 -0
- package/dist/components/ui/empty/empty.svelte.d.ts +5 -0
- package/dist/components/ui/empty/index.d.ts +7 -0
- package/dist/components/ui/empty/index.js +9 -0
- package/dist/globals.css +301 -5
- package/dist/i18n/langs/en.d.ts +25 -0
- package/dist/i18n/langs/en.js +25 -0
- package/dist/i18n/langs/pt.d.ts +25 -0
- package/dist/i18n/langs/pt.js +25 -0
- package/dist/i18n/translations.d.ts +50 -0
- package/package.json +1 -1
- package/dist/components/core/card/shared/CardStartPrice.svelte.d.ts +0 -8
- package/dist/components/pages/product-details/04/ProductDetails04.svelte +0 -65
- package/dist/components/pages/product-details/04/ProductDetails04.svelte.d.ts +0 -11
- /package/dist/components/core/card/{shared/CardStartPrice.svelte → promotion/CardPromotionPrice.svelte} +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/**
|
|
4
|
+
* Banner de apresentação da empresa/fábrica: imagem de capa, logótipo,
|
|
5
|
+
* nome, país, botão de contacto e faixa de estatísticas rápidas.
|
|
6
|
+
* Quando `isLoading` é true, exibe Skeletons.
|
|
7
|
+
* @component
|
|
8
|
+
* @example
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <ProfileBanner data={data.banner} id={data.id} {onContact} {onCopyLink} {onShare} />
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import Skeleton from "../../../ui/skeleton/skeleton.svelte";
|
|
17
|
+
import { Button } from "../../../ui/button/index.js";
|
|
18
|
+
import ImgPlaceholder from "../../../../assets/placeholder-image.png";
|
|
19
|
+
import type { ProfileBannerData, ProfileBannerTag } from "../types";
|
|
20
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
21
|
+
import { onDestroy } from "svelte";
|
|
22
|
+
import {
|
|
23
|
+
Calendar01FreeIcons,
|
|
24
|
+
Copy,
|
|
25
|
+
DashboardBrowsingIcon,
|
|
26
|
+
Email,
|
|
27
|
+
Link01FreeIcons,
|
|
28
|
+
MapPin,
|
|
29
|
+
Share,
|
|
30
|
+
WhatsappIcon,
|
|
31
|
+
} from "@hugeicons/core-free-icons";
|
|
32
|
+
|
|
33
|
+
const AUTOPLAY_MS = 8000;
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
id,
|
|
37
|
+
data,
|
|
38
|
+
isLoading = false,
|
|
39
|
+
onWhatsapp,
|
|
40
|
+
onEmail,
|
|
41
|
+
onCopyLink,
|
|
42
|
+
onShare,
|
|
43
|
+
tags,
|
|
44
|
+
}: {
|
|
45
|
+
id: string | number;
|
|
46
|
+
data: ProfileBannerData;
|
|
47
|
+
isLoading?: boolean;
|
|
48
|
+
onWhatsapp?: (id: string | number) => void;
|
|
49
|
+
onEmail?: (id: string | number) => void;
|
|
50
|
+
onCopyLink?: (id: string | number) => void;
|
|
51
|
+
onShare?: (id: string | number) => void;
|
|
52
|
+
tags?: ProfileBannerTag[];
|
|
53
|
+
} = $props();
|
|
54
|
+
|
|
55
|
+
const cover = $derived(data?.coverImages ?? []);
|
|
56
|
+
const isCover = $derived(cover.length > 0);
|
|
57
|
+
const hasMultipleCovers = $derived(cover.length > 1);
|
|
58
|
+
|
|
59
|
+
// ---- Carrossel de imagens de capa ----
|
|
60
|
+
let activeIndex = $state(0);
|
|
61
|
+
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
62
|
+
|
|
63
|
+
function stopAutoplay() {
|
|
64
|
+
if (intervalId) clearInterval(intervalId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function startAutoplay() {
|
|
68
|
+
stopAutoplay();
|
|
69
|
+
if (hasMultipleCovers) {
|
|
70
|
+
intervalId = setInterval(() => {
|
|
71
|
+
activeIndex = (activeIndex + 1) % cover.length;
|
|
72
|
+
}, AUTOPLAY_MS);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function goTo(index: number) {
|
|
77
|
+
activeIndex = index;
|
|
78
|
+
startAutoplay(); // reinicia o timer ao clicar manualmente
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Reinicia apenas quando o conjunto de imagens muda (não a cada render)
|
|
82
|
+
$effect(() => {
|
|
83
|
+
void cover.length; // dependência explícita
|
|
84
|
+
activeIndex = 0;
|
|
85
|
+
startAutoplay();
|
|
86
|
+
return stopAutoplay;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
onDestroy(stopAutoplay);
|
|
90
|
+
|
|
91
|
+
// Ações do header (topo direito) — evita repetir markup de botão
|
|
92
|
+
const headerActions = $derived([
|
|
93
|
+
{ icon: Copy, label: "Copiar link", onclick: () => onCopyLink?.(id) },
|
|
94
|
+
{ icon: Share, label: "Partilhar", onclick: () => onShare?.(id) },
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const contactActions = $derived([
|
|
98
|
+
{
|
|
99
|
+
icon: Email,
|
|
100
|
+
label: "Email",
|
|
101
|
+
iconClass: "text-red-500",
|
|
102
|
+
onclick: () => onEmail?.(id),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
icon: WhatsappIcon,
|
|
106
|
+
label: "Whatsapp",
|
|
107
|
+
iconClass: "text-green-500",
|
|
108
|
+
onclick: () => onWhatsapp?.(id),
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const infoItems = $derived([
|
|
113
|
+
{
|
|
114
|
+
title: "Categoria",
|
|
115
|
+
description: data.category,
|
|
116
|
+
icon: DashboardBrowsingIcon,
|
|
117
|
+
},
|
|
118
|
+
{ title: "Endereço", description: data.address, icon: MapPin },
|
|
119
|
+
{
|
|
120
|
+
title: "Anos de Atuação",
|
|
121
|
+
description: data.yearFounded,
|
|
122
|
+
icon: Calendar01FreeIcons,
|
|
123
|
+
},
|
|
124
|
+
{ title: "Website", description: data.website, icon: Link01FreeIcons },
|
|
125
|
+
]);
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
{#if isLoading}
|
|
129
|
+
<div class="flex flex-col gap-4">
|
|
130
|
+
<Skeleton class="aspect-21/9 w-full rounded-xl" />
|
|
131
|
+
<div class="flex flex-col items-center gap-3">
|
|
132
|
+
<Skeleton class="size-24 rounded-2xl -mt-16 border-4 border-background" />
|
|
133
|
+
<Skeleton class="h-7 w-48 rounded-md" />
|
|
134
|
+
<Skeleton class="h-10 w-44 rounded-md" />
|
|
135
|
+
</div>
|
|
136
|
+
<div class="flex justify-center gap-6">
|
|
137
|
+
{#each Array.from({ length: 4 }) as _, i (i)}
|
|
138
|
+
<Skeleton class="h-12 w-24 rounded-md" />
|
|
139
|
+
{/each}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
{:else}
|
|
143
|
+
<div
|
|
144
|
+
class="flex flex-col items-center justify-center p-2 relative rounded-lg overflow-hidden
|
|
145
|
+
{isCover ? '' : 'bg-gradient'}"
|
|
146
|
+
>
|
|
147
|
+
{#if isCover}
|
|
148
|
+
<div class="absolute inset-0 -z-10">
|
|
149
|
+
{#each cover as image, i (image.src)}
|
|
150
|
+
<img
|
|
151
|
+
src={image.src}
|
|
152
|
+
alt={image.alt ?? ""}
|
|
153
|
+
class="absolute inset-0 h-full w-full object-cover transition-opacity duration-700 ease-in-out {i ===
|
|
154
|
+
activeIndex
|
|
155
|
+
? 'opacity-100'
|
|
156
|
+
: 'opacity-0 pointer-events-none'}"
|
|
157
|
+
/>
|
|
158
|
+
{/each}
|
|
159
|
+
<div class="absolute inset-0 bg-black/30"></div>
|
|
160
|
+
|
|
161
|
+
{#if hasMultipleCovers}
|
|
162
|
+
<div
|
|
163
|
+
class="absolute bottom-3 left-1/2 z-10 flex -translate-x-1/2 gap-1.5"
|
|
164
|
+
>
|
|
165
|
+
{#each cover as _, i (i)}
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
aria-label={`Ir para imagem ${i + 1}`}
|
|
169
|
+
onclick={() => goTo(i)}
|
|
170
|
+
class="h-1.5 rounded-full transition-all {i === activeIndex
|
|
171
|
+
? 'w-4 bg-white'
|
|
172
|
+
: 'w-1.5 bg-white/50'}"
|
|
173
|
+
></button>
|
|
174
|
+
{/each}
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
<div class="overflow-hidden rounded-xl absolute top-0 right-0">
|
|
181
|
+
<div class="flex gap-2 md:gap-5 p-4">
|
|
182
|
+
{#each headerActions as action (action.label)}
|
|
183
|
+
<Button
|
|
184
|
+
type="button"
|
|
185
|
+
variant="ghost"
|
|
186
|
+
class="rounded-full bg-white dark:bg-background"
|
|
187
|
+
aria-label={action.label}
|
|
188
|
+
onclick={action.onclick}
|
|
189
|
+
>
|
|
190
|
+
<HugeiconsIcon icon={action.icon} />
|
|
191
|
+
</Button>
|
|
192
|
+
{/each}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div
|
|
197
|
+
class="flex flex-col items-center gap-3 px-4 text-center justify-center md:-mb-10"
|
|
198
|
+
>
|
|
199
|
+
<div class="flex shrink-0 items-center justify-center">
|
|
200
|
+
{#if data.logo}
|
|
201
|
+
<img
|
|
202
|
+
src={data.logo}
|
|
203
|
+
alt={data.name}
|
|
204
|
+
class="rounded-full w-15 md:w-25 h-15 md:h-25 lg:w-40 lg:h-40 p-2"
|
|
205
|
+
onerror={(e) =>
|
|
206
|
+
((e.target as HTMLImageElement).src = ImgPlaceholder)}
|
|
207
|
+
/>
|
|
208
|
+
{:else}
|
|
209
|
+
<span class="text-2xl font-bold text-muted-foreground"
|
|
210
|
+
>{data.name?.[0] ?? "?"}</span
|
|
211
|
+
>
|
|
212
|
+
{/if}
|
|
213
|
+
</div>
|
|
214
|
+
<h1 class="text-lg font-bold tracking-tight md:text-xl mb-5 text-white">
|
|
215
|
+
{data.name}
|
|
216
|
+
</h1>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div class="flex justify-between w-full p-5">
|
|
220
|
+
{#each contactActions as action (action.label)}
|
|
221
|
+
<Button
|
|
222
|
+
variant="ghost"
|
|
223
|
+
size="sm"
|
|
224
|
+
onclick={action.onclick}
|
|
225
|
+
class="bg-white cursor-pointer"
|
|
226
|
+
>
|
|
227
|
+
<HugeiconsIcon icon={action.icon} class={action.iconClass} />
|
|
228
|
+
{action.label}
|
|
229
|
+
</Button>
|
|
230
|
+
{/each}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div
|
|
235
|
+
class="bg-gray-50 dark:bg-background p-2 md:p-10 flex flex-col items-center justify-center"
|
|
236
|
+
>
|
|
237
|
+
<div
|
|
238
|
+
class="grid grid-cols-2 items-center w-full
|
|
239
|
+
[&>*:nth-child(odd)]:border-r
|
|
240
|
+
[&>*:nth-child(-n+2)]:pb-1 [&>*:nth-child(-n+2)]:md:pb-2.5 [&>*:nth-child(-n+2)]:lg:pb-5
|
|
241
|
+
[&>*:nth-child(-n+2)]:border-b
|
|
242
|
+
[&>*:nth-child(n+3)]:pt-1 [&>*:nth-child(n+3)]:md:pt-2.5 [&>*:nth-child(n+3)]:lg:pt-5
|
|
243
|
+
*:border-border
|
|
244
|
+
*:w-full
|
|
245
|
+
*:flex *:flex-col *:items-center"
|
|
246
|
+
>
|
|
247
|
+
{#each infoItems as item (item.title)}
|
|
248
|
+
{@render companyInfo(item)}
|
|
249
|
+
{/each}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
{/if}
|
|
253
|
+
|
|
254
|
+
{#snippet companyInfo({
|
|
255
|
+
title,
|
|
256
|
+
description,
|
|
257
|
+
icon,
|
|
258
|
+
}: {
|
|
259
|
+
title: string;
|
|
260
|
+
description?: string;
|
|
261
|
+
icon: any;
|
|
262
|
+
})}
|
|
263
|
+
<div class="flex flex-col gap-2 items-center">
|
|
264
|
+
<h3 class="text-md font-semibold flex gap-2 items-center">
|
|
265
|
+
<HugeiconsIcon {icon} size={15} />
|
|
266
|
+
{title}
|
|
267
|
+
</h3>
|
|
268
|
+
<p class="text-md text-gray-400">{description ?? "---"}</p>
|
|
269
|
+
</div>
|
|
270
|
+
{/snippet}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ProfileBannerData, ProfileBannerTag } from "../types";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
id: string | number;
|
|
4
|
+
data: ProfileBannerData;
|
|
5
|
+
isLoading?: boolean;
|
|
6
|
+
onWhatsapp?: (id: string | number) => void;
|
|
7
|
+
onEmail?: (id: string | number) => void;
|
|
8
|
+
onCopyLink?: (id: string | number) => void;
|
|
9
|
+
onShare?: (id: string | number) => void;
|
|
10
|
+
tags?: ProfileBannerTag[];
|
|
11
|
+
};
|
|
12
|
+
declare const ProfileBanner: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type ProfileBanner = ReturnType<typeof ProfileBanner>;
|
|
14
|
+
export default ProfileBanner;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Secção "Galeria": grelha de imagens/vídeos da empresa com lightbox simples.
|
|
4
|
+
* O vídeo (se existir) ocupa sempre o card em destaque.
|
|
5
|
+
* Quando `isLoading` é true, exibe Skeletons.
|
|
6
|
+
* @component
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <ProfileGallery data={data.gallery} />
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import Skeleton from "../../../ui/skeleton/skeleton.svelte";
|
|
16
|
+
import type { ProfileGalleryData, ProfileImage } from "../types";
|
|
17
|
+
import ImgPlaceholder from "../../../../assets/placeholder-image.png";
|
|
18
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
19
|
+
import { GalleryThumbnailsFreeIcons } from "@hugeicons/core-free-icons";
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
data,
|
|
23
|
+
isLoading = false,
|
|
24
|
+
className,
|
|
25
|
+
}: {
|
|
26
|
+
data: ProfileGalleryData;
|
|
27
|
+
isLoading?: boolean;
|
|
28
|
+
className?: string;
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
// Vídeo primeiro (ocupa o card em destaque), resto mantém a ordem original.
|
|
32
|
+
const images = $derived(
|
|
33
|
+
[...(data?.images ?? [])].sort((a, b) =>
|
|
34
|
+
a.type === "video" ? -1 : b.type === "video" ? 1 : 0,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
const visible = $derived(images.slice(0, 5));
|
|
38
|
+
|
|
39
|
+
let index = $state<number | null>(null);
|
|
40
|
+
const active = $derived(index !== null ? images[index] : undefined);
|
|
41
|
+
|
|
42
|
+
const open = (i: number) => (index = i);
|
|
43
|
+
const close = () => (index = null);
|
|
44
|
+
const step = (dir: 1 | -1) => {
|
|
45
|
+
if (index === null || !images.length) return;
|
|
46
|
+
index = (index + dir + images.length) % images.length;
|
|
47
|
+
};
|
|
48
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
49
|
+
if (index === null) return;
|
|
50
|
+
if (e.key === "Escape") close();
|
|
51
|
+
else if (e.key === "ArrowRight") step(1);
|
|
52
|
+
else if (e.key === "ArrowLeft") step(-1);
|
|
53
|
+
};
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<svelte:window onkeydown={onKeydown} />
|
|
57
|
+
|
|
58
|
+
{#snippet media(item: ProfileImage, cls: string)}
|
|
59
|
+
{#if item.type === "video"}
|
|
60
|
+
<video src={item.src} class={cls} muted playsinline preload="metadata"
|
|
61
|
+
></video>
|
|
62
|
+
<span
|
|
63
|
+
class="absolute inset-0 flex items-center justify-center bg-black/20 transition-colors group-hover:bg-black/30"
|
|
64
|
+
>
|
|
65
|
+
<span
|
|
66
|
+
class="flex size-11 items-center justify-center rounded-full bg-black/50 text-white"
|
|
67
|
+
>
|
|
68
|
+
<svg viewBox="0 0 24 24" class="size-5 fill-white" aria-hidden="true">
|
|
69
|
+
<path d="M8 5v14l11-7z" />
|
|
70
|
+
</svg>
|
|
71
|
+
</span>
|
|
72
|
+
</span>
|
|
73
|
+
{:else}
|
|
74
|
+
<img
|
|
75
|
+
src={item.src}
|
|
76
|
+
alt={item.alt ?? ""}
|
|
77
|
+
class={cls}
|
|
78
|
+
loading="lazy"
|
|
79
|
+
decoding="async"
|
|
80
|
+
onerror={(e) => ((e.target as HTMLImageElement).src = ImgPlaceholder)}
|
|
81
|
+
/>
|
|
82
|
+
{/if}
|
|
83
|
+
{/snippet}
|
|
84
|
+
|
|
85
|
+
{#snippet arrow(dir: 1 | -1, side: string)}
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
aria-label={dir === -1 ? "Imagem anterior" : "Próxima imagem"}
|
|
89
|
+
onclick={() => step(dir)}
|
|
90
|
+
class="absolute {side} flex size-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
|
|
91
|
+
>
|
|
92
|
+
<svg
|
|
93
|
+
viewBox="0 0 24 24"
|
|
94
|
+
class="size-5"
|
|
95
|
+
fill="none"
|
|
96
|
+
stroke="currentColor"
|
|
97
|
+
stroke-width="2"
|
|
98
|
+
stroke-linecap="round"
|
|
99
|
+
stroke-linejoin="round"
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
>
|
|
102
|
+
<path d={dir === -1 ? "m15 18-6-6 6-6" : "m9 18 6-6-6-6"} />
|
|
103
|
+
</svg>
|
|
104
|
+
</button>
|
|
105
|
+
{/snippet}
|
|
106
|
+
|
|
107
|
+
<div class="bg-gray-50 dark:bg-background p-4 md:p-6 {className}">
|
|
108
|
+
<h2 class="mb-4 flex items-center gap-2 text-lg font-bold">
|
|
109
|
+
<HugeiconsIcon icon={GalleryThumbnailsFreeIcons} />
|
|
110
|
+
{data.title ?? "Galeria"}
|
|
111
|
+
</h2>
|
|
112
|
+
|
|
113
|
+
{#if isLoading}
|
|
114
|
+
<div class="grid grid-cols-2 gap-3 md:grid-cols-3">
|
|
115
|
+
<Skeleton
|
|
116
|
+
class="col-span-2 row-span-2 aspect-square rounded-lg md:aspect-auto"
|
|
117
|
+
/>
|
|
118
|
+
{#each Array.from({ length: 4 }) as _, i (i)}
|
|
119
|
+
<Skeleton class="aspect-square rounded-lg" />
|
|
120
|
+
{/each}
|
|
121
|
+
</div>
|
|
122
|
+
{:else if visible.length}
|
|
123
|
+
<div class="grid grid-cols-2 gap-3 md:grid-cols-3 md:auto-rows-36">
|
|
124
|
+
{#each visible as item, i (item.src)}
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onclick={() => open(i)}
|
|
128
|
+
class="group relative overflow-hidden rounded-lg border {i === 0
|
|
129
|
+
? 'col-span-2 row-span-2'
|
|
130
|
+
: ''}"
|
|
131
|
+
aria-label={item.alt ??
|
|
132
|
+
(item.type === "video"
|
|
133
|
+
? "Reproduzir vídeo"
|
|
134
|
+
: `Ver imagem ${i + 1}`)}
|
|
135
|
+
>
|
|
136
|
+
{@render media(
|
|
137
|
+
item,
|
|
138
|
+
"size-full object-cover transition-transform duration-300 group-hover:scale-105",
|
|
139
|
+
)}
|
|
140
|
+
</button>
|
|
141
|
+
{/each}
|
|
142
|
+
</div>
|
|
143
|
+
{:else}
|
|
144
|
+
<p class="text-sm text-muted-foreground">
|
|
145
|
+
Galeria ainda não foi submetida.
|
|
146
|
+
</p>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{#if active}
|
|
151
|
+
<div
|
|
152
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
|
|
153
|
+
role="dialog"
|
|
154
|
+
aria-modal="true"
|
|
155
|
+
>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
aria-label="Fechar"
|
|
159
|
+
onclick={close}
|
|
160
|
+
class="absolute right-4 top-4 flex size-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
|
|
161
|
+
>
|
|
162
|
+
<svg
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
class="size-5"
|
|
165
|
+
fill="none"
|
|
166
|
+
stroke="currentColor"
|
|
167
|
+
stroke-width="2"
|
|
168
|
+
stroke-linecap="round"
|
|
169
|
+
aria-hidden="true"
|
|
170
|
+
>
|
|
171
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
172
|
+
</svg>
|
|
173
|
+
</button>
|
|
174
|
+
|
|
175
|
+
{#if images.length > 1}
|
|
176
|
+
{@render arrow(-1, "left-2 md:left-6")}
|
|
177
|
+
{@render arrow(1, "right-2 md:right-6")}
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
{#if active.type === "video"}
|
|
181
|
+
<!-- svelte-ignore a11y_media_has_caption -->
|
|
182
|
+
<video
|
|
183
|
+
src={active.src}
|
|
184
|
+
class="max-h-[85vh] max-w-full rounded-lg object-contain"
|
|
185
|
+
controls
|
|
186
|
+
autoplay
|
|
187
|
+
playsinline
|
|
188
|
+
></video>
|
|
189
|
+
{:else}
|
|
190
|
+
<img
|
|
191
|
+
src={active.src}
|
|
192
|
+
alt={active.alt ?? ""}
|
|
193
|
+
class="max-h-[85vh] max-w-full rounded-lg object-contain"
|
|
194
|
+
onerror={(e) => ((e.target as HTMLImageElement).src = ImgPlaceholder)}
|
|
195
|
+
/>
|
|
196
|
+
{/if}
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProfileGalleryData } from "../types";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
data: ProfileGalleryData;
|
|
4
|
+
isLoading?: boolean;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const ProfileGallery: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type ProfileGallery = ReturnType<typeof ProfileGallery>;
|
|
9
|
+
export default ProfileGallery;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Menu de navegação entre secções do perfil em formato de badges,
|
|
4
|
+
* usado em mobile no lugar da `ProfileSidebar`. Clicar num badge
|
|
5
|
+
* desloca a página até à secção correspondente.
|
|
6
|
+
* Quando `isLoading` é true, exibe Skeletons.
|
|
7
|
+
* @component
|
|
8
|
+
* @example
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <ProfileMenuBadge items={sections} active={activeSection} onSelect={scrollToSection} />
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import Skeleton from "../../../ui/skeleton/skeleton.svelte";
|
|
17
|
+
import type { ProfileNavItem } from "../types";
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
items,
|
|
21
|
+
active,
|
|
22
|
+
isLoading = false,
|
|
23
|
+
onSelect,
|
|
24
|
+
className,
|
|
25
|
+
}: {
|
|
26
|
+
items: ProfileNavItem[];
|
|
27
|
+
active?: string;
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
onSelect?: (id: string) => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
} = $props();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
class="sticky top-0 z-10 -mx-4 flex gap-2 overflow-x-auto border-b bg-background/95 px-4 py-3 no-scrollbar backdrop-blur {className ?? ''}"
|
|
36
|
+
>
|
|
37
|
+
{#if isLoading}
|
|
38
|
+
{#each Array.from({ length: 3 }) as _, i (i)}
|
|
39
|
+
<Skeleton class="h-8 w-28 shrink-0 rounded-full" />
|
|
40
|
+
{/each}
|
|
41
|
+
{:else}
|
|
42
|
+
{#each items as item (item.id)}
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onclick={() => onSelect?.(item.id)}
|
|
46
|
+
aria-current={active === item.id ? "true" : undefined}
|
|
47
|
+
class="shrink-0 whitespace-nowrap rounded-full border px-4 py-1.5 text-sm font-medium transition-colors {active ===
|
|
48
|
+
item.id
|
|
49
|
+
? 'border-transparent bg-gradient text-white'
|
|
50
|
+
: 'border-border text-muted-foreground hover:text-foreground'}"
|
|
51
|
+
>
|
|
52
|
+
{item.label}
|
|
53
|
+
</button>
|
|
54
|
+
{/each}
|
|
55
|
+
{/if}
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProfileNavItem } from "../types";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
items: ProfileNavItem[];
|
|
4
|
+
active?: string;
|
|
5
|
+
isLoading?: boolean;
|
|
6
|
+
onSelect?: (id: string) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const ProfileMenuBadge: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type ProfileMenuBadge = ReturnType<typeof ProfileMenuBadge>;
|
|
11
|
+
export default ProfileMenuBadge;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Secção "Produtos": exibe a lista de produtos/serviços da empresa
|
|
4
|
+
* utilizando o componente CarouselGridProduct.
|
|
5
|
+
* Quando `isLoading` é true, exibe Skeletons.
|
|
6
|
+
* @component
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <ProfileProducts data={data.products} />
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import CarouselGridProduct from "../../../core/carousel/grid/product/ui/CarouselGridProduct.svelte";
|
|
16
|
+
import type { ProfileProductsData } from "../types";
|
|
17
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
18
|
+
import { PackageIcon } from "@hugeicons/core-free-icons";
|
|
19
|
+
import NotFoundEmpty from "../../../core/panel/NotFoundEmpty.svelte";
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
data,
|
|
23
|
+
isLoading = false,
|
|
24
|
+
className = "",
|
|
25
|
+
}: {
|
|
26
|
+
data?: ProfileProductsData;
|
|
27
|
+
isLoading?: boolean;
|
|
28
|
+
className?: string;
|
|
29
|
+
} = $props();
|
|
30
|
+
|
|
31
|
+
const items = $derived(data?.items ?? []);
|
|
32
|
+
const loading = $derived(isLoading || Boolean(data?.isLoading));
|
|
33
|
+
const variant = $derived(data?.variant ?? data?.varient ?? 1);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div class="bg-gray-50 dark:bg-background p-4 md:p-6 rounded-lg {className}">
|
|
37
|
+
<h2 class="mb-4 flex items-center gap-2 text-lg font-bold">
|
|
38
|
+
<HugeiconsIcon icon={PackageIcon} class="size-5" />
|
|
39
|
+
{data?.title ?? "Produtos"}
|
|
40
|
+
</h2>
|
|
41
|
+
|
|
42
|
+
{#if !loading && items.length === 0}
|
|
43
|
+
<NotFoundEmpty />
|
|
44
|
+
{:else}
|
|
45
|
+
<CarouselGridProduct
|
|
46
|
+
{items}
|
|
47
|
+
{variant}
|
|
48
|
+
isLoading={loading}
|
|
49
|
+
headerProps={{
|
|
50
|
+
title: "",
|
|
51
|
+
description: data?.description,
|
|
52
|
+
containerClass: "p-0 bg-transparent",
|
|
53
|
+
}}
|
|
54
|
+
slotProps={{
|
|
55
|
+
buttonPreviousAndNextClass:
|
|
56
|
+
"bg-white dark:bg-gray-800 shadow-md hover:bg-gray-100 dark:hover:bg-gray-700",
|
|
57
|
+
positionButtonPreviousAndNext: "center",
|
|
58
|
+
}}
|
|
59
|
+
gridClass="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
|
|
60
|
+
/>
|
|
61
|
+
{/if}
|
|
62
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProfileProductsData } from "../types";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
data?: ProfileProductsData;
|
|
4
|
+
isLoading?: boolean;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const ProfileProducts: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type ProfileProducts = ReturnType<typeof ProfileProducts>;
|
|
9
|
+
export default ProfileProducts;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* Barra lateral de navegação entre secções do perfil da empresa.
|
|
4
|
+
* Visível apenas em ecrãs médios/grandes — em mobile usar `ProfileMenuBadge`.
|
|
5
|
+
* Quando `isLoading` é true, exibe Skeletons.
|
|
6
|
+
* @component
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <ProfileSidebar items={sections} active={activeSection} onSelect={scrollToSection} />
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import Skeleton from "../../../ui/skeleton/skeleton.svelte";
|
|
16
|
+
import type { ProfileNavItem } from "../types";
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
items,
|
|
20
|
+
active,
|
|
21
|
+
isLoading = false,
|
|
22
|
+
onSelect,
|
|
23
|
+
className,
|
|
24
|
+
}: {
|
|
25
|
+
items: ProfileNavItem[];
|
|
26
|
+
active?: string;
|
|
27
|
+
isLoading?: boolean;
|
|
28
|
+
onSelect?: (id: string) => void;
|
|
29
|
+
className?: string;
|
|
30
|
+
} = $props();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<nav aria-label="Navegação do perfil" class="w-40 shrink-0 {className ?? ''}">
|
|
34
|
+
<div
|
|
35
|
+
class="fixed flex flex-col gap-1 md:pt-10 lg:pt-20 md:gap-2 bg-card p-6 md:p-8 h-screen border-r"
|
|
36
|
+
>
|
|
37
|
+
{#if isLoading}
|
|
38
|
+
{#each Array.from({ length: 4 }) as _, i (i)}
|
|
39
|
+
<Skeleton class="h-9 w-full rounded-md" />
|
|
40
|
+
{/each}
|
|
41
|
+
{:else}
|
|
42
|
+
{#each items as item (item.id)}
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onclick={() => onSelect?.(item.id)}
|
|
46
|
+
aria-current={active === item.id ? "true" : undefined}
|
|
47
|
+
class="flex items-center gap-2 rounded-sm px-3 py-2 text-left text-sm font-medium transition-colors {active ===
|
|
48
|
+
item.id
|
|
49
|
+
? 'bg-blue-100'
|
|
50
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
|
|
51
|
+
>
|
|
52
|
+
{item.label}
|
|
53
|
+
</button>
|
|
54
|
+
{/each}
|
|
55
|
+
{/if}
|
|
56
|
+
</div>
|
|
57
|
+
</nav>
|