negodesign 0.0.35 → 0.0.36
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/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 +61 -0
- package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte.d.ts +4 -0
- 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 +50 -0
- package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte.d.ts +4 -0
- 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 +47 -0
- package/dist/components/core/carousel/product/ui/CarouselProduct.svelte.d.ts +4 -0
- package/dist/components/pages/company-profile/CompanyProfile.svelte +140 -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 +68 -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 +194 -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/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 +117 -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/globals.css +244 -5
- package/dist/i18n/langs/en.d.ts +10 -0
- package/dist/i18n/langs/en.js +10 -0
- package/dist/i18n/langs/pt.d.ts +10 -0
- package/dist/i18n/langs/pt.js +10 -0
- package/dist/i18n/translations.d.ts +20 -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,194 @@
|
|
|
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 {
|
|
22
|
+
Calendar01FreeIcons,
|
|
23
|
+
Copy,
|
|
24
|
+
DashboardBrowsingIcon,
|
|
25
|
+
Email,
|
|
26
|
+
Link01FreeIcons,
|
|
27
|
+
MapPin,
|
|
28
|
+
Share,
|
|
29
|
+
WhatsappIcon,
|
|
30
|
+
} from "@hugeicons/core-free-icons";
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
id,
|
|
34
|
+
data,
|
|
35
|
+
isLoading = false,
|
|
36
|
+
onWhatsapp,
|
|
37
|
+
onEmail,
|
|
38
|
+
onCopyLink,
|
|
39
|
+
onShare,
|
|
40
|
+
tags,
|
|
41
|
+
}: {
|
|
42
|
+
id: string | number;
|
|
43
|
+
data: ProfileBannerData;
|
|
44
|
+
isLoading?: boolean;
|
|
45
|
+
onWhatsapp?: (id: string | number) => void;
|
|
46
|
+
onEmail?: (id: string | number) => void;
|
|
47
|
+
onCopyLink?: (id: string | number) => void;
|
|
48
|
+
onShare?: (id: string | number) => void;
|
|
49
|
+
tags?: ProfileBannerTag[];
|
|
50
|
+
} = $props();
|
|
51
|
+
|
|
52
|
+
const cover = $derived(data?.coverImages ?? []);
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#if isLoading}
|
|
56
|
+
<div class="flex flex-col gap-4">
|
|
57
|
+
<Skeleton class="aspect-21/9 w-full rounded-xl" />
|
|
58
|
+
<div class="flex flex-col items-center gap-3">
|
|
59
|
+
<Skeleton class="size-24 rounded-2xl -mt-16 border-4 border-background" />
|
|
60
|
+
<Skeleton class="h-7 w-48 rounded-md" />
|
|
61
|
+
<Skeleton class="h-10 w-44 rounded-md" />
|
|
62
|
+
</div>
|
|
63
|
+
<div class="flex justify-center gap-6">
|
|
64
|
+
{#each Array.from({ length: 4 }) as _, i (i)}
|
|
65
|
+
<Skeleton class="h-12 w-24 rounded-md" />
|
|
66
|
+
{/each}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
{:else}
|
|
70
|
+
<div
|
|
71
|
+
class="flex flex-col items-center justify-center bg-gradient p-2 relative rounded-lg"
|
|
72
|
+
>
|
|
73
|
+
<!-- Capa -->
|
|
74
|
+
<div class="overflow-hidden rounded-xl absolute top-0 right-0">
|
|
75
|
+
<div class="flex gap-2 md:gap-5 p-4">
|
|
76
|
+
<Button
|
|
77
|
+
type="button"
|
|
78
|
+
variant="ghost"
|
|
79
|
+
class="rounded-full bg-white dark:bg-background"
|
|
80
|
+
aria-label="Copiar link"
|
|
81
|
+
onclick={() => onCopyLink?.(id)}
|
|
82
|
+
>
|
|
83
|
+
<HugeiconsIcon icon={Copy} />
|
|
84
|
+
</Button>
|
|
85
|
+
<Button
|
|
86
|
+
type="button"
|
|
87
|
+
variant="ghost"
|
|
88
|
+
class="rounded-full bg-white dark:bg-background"
|
|
89
|
+
aria-label="Partilhar"
|
|
90
|
+
onclick={() => onShare?.(id)}
|
|
91
|
+
>
|
|
92
|
+
<HugeiconsIcon icon={Share} />
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Logótipo + nome + contacto -->
|
|
98
|
+
<div
|
|
99
|
+
class="flex flex-col items-center gap-3 px-4 text-center justify-center md:-mb-10"
|
|
100
|
+
>
|
|
101
|
+
<div class="flex shrink-0 items-center justify-center">
|
|
102
|
+
{#if data.logo}
|
|
103
|
+
<img
|
|
104
|
+
src={data.logo}
|
|
105
|
+
alt={data.name}
|
|
106
|
+
class="rounded-full w-15 md:w-25 h-15 md:h-25 p-2"
|
|
107
|
+
onerror={(e) =>
|
|
108
|
+
((e.target as HTMLImageElement).src = ImgPlaceholder)}
|
|
109
|
+
/>
|
|
110
|
+
{:else}
|
|
111
|
+
<span class="text-2xl font-bold text-muted-foreground"
|
|
112
|
+
>{data.name?.[0] ?? "?"}</span
|
|
113
|
+
>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
116
|
+
<h1 class="text-lg font-bold tracking-tight md:text-xl mb-5 text-white">
|
|
117
|
+
{data.name}
|
|
118
|
+
</h1>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="flex justify-between w-full p-5">
|
|
121
|
+
<Button
|
|
122
|
+
variant="ghost"
|
|
123
|
+
size="sm"
|
|
124
|
+
onclick={() => onEmail?.(id)}
|
|
125
|
+
class="bg-white cursor-pointer"
|
|
126
|
+
>
|
|
127
|
+
<HugeiconsIcon icon={Email} class="text-red-500" />
|
|
128
|
+
Email
|
|
129
|
+
</Button>
|
|
130
|
+
<Button
|
|
131
|
+
variant="ghost"
|
|
132
|
+
size="sm"
|
|
133
|
+
onclick={() => onWhatsapp?.(id)}
|
|
134
|
+
class="bg-white cursor-pointer"
|
|
135
|
+
>
|
|
136
|
+
<HugeiconsIcon icon={WhatsappIcon} class="text-green-500" />
|
|
137
|
+
Whatsapp
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div
|
|
142
|
+
class="bg-gray-50 dark:bg-background p-2 md:p-10 flex flex-col items-center justify-center"
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
class="grid grid-cols-2 items-center w-full
|
|
146
|
+
[&>*:nth-child(odd)]:border-r
|
|
147
|
+
[&>*:nth-child(-n+2)]:pb-1 [&>*:nth-child(-n+2)]:md:pb-2.5 [&>*:nth-child(-n+2)]:lg:pb-5
|
|
148
|
+
[&>*:nth-child(-n+2)]:border-b
|
|
149
|
+
[&>*:nth-child(n+3)]:pt-1 [&>*:nth-child(n+3)]:md:pt-2.5 [&>*:nth-child(n+3)]:lg:pt-5
|
|
150
|
+
*:border-border
|
|
151
|
+
*:w-full
|
|
152
|
+
*:flex *:flex-col *:items-center"
|
|
153
|
+
>
|
|
154
|
+
{@render companyInfo({
|
|
155
|
+
title: "Categoria",
|
|
156
|
+
description: data.category,
|
|
157
|
+
icon: DashboardBrowsingIcon,
|
|
158
|
+
})}
|
|
159
|
+
{@render companyInfo({
|
|
160
|
+
title: "Endereço",
|
|
161
|
+
description: data.address,
|
|
162
|
+
icon: MapPin,
|
|
163
|
+
})}
|
|
164
|
+
{@render companyInfo({
|
|
165
|
+
title: "Anos de Atuação",
|
|
166
|
+
description: data.yearFounded,
|
|
167
|
+
icon: Calendar01FreeIcons,
|
|
168
|
+
})}
|
|
169
|
+
{@render companyInfo({
|
|
170
|
+
title: "Website",
|
|
171
|
+
description: data.website,
|
|
172
|
+
icon: Link01FreeIcons,
|
|
173
|
+
})}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
|
|
178
|
+
{#snippet companyInfo({
|
|
179
|
+
title,
|
|
180
|
+
description,
|
|
181
|
+
icon,
|
|
182
|
+
}: {
|
|
183
|
+
title: string;
|
|
184
|
+
description?: string;
|
|
185
|
+
icon: any;
|
|
186
|
+
})}
|
|
187
|
+
<div class="flex flex-col gap-2 items-center">
|
|
188
|
+
<h3 class="text-md font-semibold flex gap-2 items-center">
|
|
189
|
+
<HugeiconsIcon {icon} size={15} />
|
|
190
|
+
{title}
|
|
191
|
+
</h3>
|
|
192
|
+
<p class="text-md text-gray-400">{description ?? "---"}</p>
|
|
193
|
+
</div>
|
|
194
|
+
{/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,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: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>
|
|
@@ -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 ProfileSidebar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type ProfileSidebar = ReturnType<typeof ProfileSidebar>;
|
|
11
|
+
export default ProfileSidebar;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { IconSvgElement } from "@hugeicons/svelte";
|
|
2
|
+
/**
|
|
3
|
+
* Item de estatística exibido na faixa de dados do banner
|
|
4
|
+
* (ex: "Setor de atuação", "Anos de Atuação", "Localização").
|
|
5
|
+
*/
|
|
6
|
+
export interface ProfileStatItem {
|
|
7
|
+
/** Rótulo da estatística (ex: "Setor de atuação"). */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Valor exibido (ex: "Papel", "8", "Benguela"). */
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Imagem usada na galeria ou na capa do perfil.
|
|
14
|
+
*/
|
|
15
|
+
export interface ProfileImage {
|
|
16
|
+
/** Caminho ou URL completa da imagem (obrigatório). */
|
|
17
|
+
src: string;
|
|
18
|
+
/** Texto alternativo para acessibilidade. */
|
|
19
|
+
alt?: string;
|
|
20
|
+
type?: "image" | "video";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Item de navegação usado pela Sidebar e pelo menu de badges (mobile).
|
|
24
|
+
* O `id` deve corresponder ao `id` da secção (elemento âncora) na página.
|
|
25
|
+
*/
|
|
26
|
+
export interface ProfileNavItem {
|
|
27
|
+
/** Identificador único da secção (usado como âncora de scroll). */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Rótulo visível do item de navegação. */
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dados do banner/apresentação da empresa: capa, logótipo, nome,
|
|
34
|
+
* país, botão de contacto e faixa de estatísticas.
|
|
35
|
+
*/
|
|
36
|
+
export interface ProfileBannerData {
|
|
37
|
+
address?: string;
|
|
38
|
+
category?: string;
|
|
39
|
+
website?: string;
|
|
40
|
+
yearFounded?: string;
|
|
41
|
+
/** Imagem(ns) de capa exibidas no topo do banner. */
|
|
42
|
+
coverImages: ProfileImage[];
|
|
43
|
+
/** URL do logótipo da empresa. */
|
|
44
|
+
logo?: string;
|
|
45
|
+
/** Nome da empresa/fábrica. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** Nome do país (ex: "Angola"). */
|
|
48
|
+
countryLabel?: string;
|
|
49
|
+
/** Emoji ou URL da bandeira do país. */
|
|
50
|
+
countryFlag?: string;
|
|
51
|
+
/** Texto do botão de contacto (padrão: "Contactar fábrica"). */
|
|
52
|
+
contactLabel?: string;
|
|
53
|
+
/** Estatísticas rápidas exibidas por baixo do nome (sector, anos, etc). */
|
|
54
|
+
stats?: ProfileStatItem[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Dados da secção "Sobre a Empresa".
|
|
58
|
+
*/
|
|
59
|
+
export interface ProfileAboutData {
|
|
60
|
+
/** Imagem ilustrativa da empresa (ex: fachada, fábrica). */
|
|
61
|
+
image?: string;
|
|
62
|
+
/** Texto alternativo da imagem. */
|
|
63
|
+
imageAlt?: string;
|
|
64
|
+
/** Título da secção (padrão: "Sobre a Empresa"). */
|
|
65
|
+
title?: string;
|
|
66
|
+
/** Parágrafos de texto descritivo da empresa. */
|
|
67
|
+
paragraphs: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Dados da secção "Galeria".
|
|
71
|
+
*/
|
|
72
|
+
export interface ProfileGalleryData {
|
|
73
|
+
/** Título da secção (padrão: "Galeria da {name}"). */
|
|
74
|
+
title?: string;
|
|
75
|
+
/** Lista de imagens/vídeos da galeria. */
|
|
76
|
+
images: ProfileImage[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Dados completos apresentados na página de perfil da empresa/fábrica.
|
|
80
|
+
*/
|
|
81
|
+
export interface CompanyProfileData {
|
|
82
|
+
/** Identificador único do perfil. */
|
|
83
|
+
id: string | number;
|
|
84
|
+
/** Dados do banner (capa, nome, estatísticas). */
|
|
85
|
+
banner: ProfileBannerData;
|
|
86
|
+
/** Dados da secção "Sobre a Empresa". */
|
|
87
|
+
about: ProfileAboutData;
|
|
88
|
+
/** Dados da secção "Galeria". */
|
|
89
|
+
gallery: ProfileGalleryData;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Propriedades do componente principal CompanyProfile.
|
|
93
|
+
*/
|
|
94
|
+
export interface CompanyProfileProps {
|
|
95
|
+
address?: string;
|
|
96
|
+
category?: string;
|
|
97
|
+
website?: string;
|
|
98
|
+
yearFounded?: string;
|
|
99
|
+
/** Dados completos do perfil a serem exibidos. */
|
|
100
|
+
data?: CompanyProfileData;
|
|
101
|
+
/**
|
|
102
|
+
* Estado de carregamento.
|
|
103
|
+
* Quando true, todos os blocos exibem Skeletons no lugar do conteúdo.
|
|
104
|
+
* @default false
|
|
105
|
+
*/
|
|
106
|
+
isLoading?: boolean;
|
|
107
|
+
onWhatsapp?: (id: string | number) => void;
|
|
108
|
+
onEmail?: (id: string | number) => void; /** Callback executado quando o utilizador clica em copiar link. */
|
|
109
|
+
onCopyLink?: (id: string | number) => void;
|
|
110
|
+
/** Callback executado quando o utilizador clica em partilhar. */
|
|
111
|
+
onShare?: (id: string | number) => void;
|
|
112
|
+
}
|
|
113
|
+
export interface ProfileBannerTag {
|
|
114
|
+
label: string;
|
|
115
|
+
value: string;
|
|
116
|
+
icon?: string | IconSvgElement;
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|