negodesign 0.0.36 → 0.0.38
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/shared/CardDescription.svelte +1 -1
- package/dist/components/core/carousel/grid/media/ui/CarouselGridMedia.svelte +38 -28
- package/dist/components/core/carousel/grid/product/ui/CarouselGridProduct.svelte +48 -38
- package/dist/components/core/carousel/grid/profile/ui/CarouselGridProfile.svelte +56 -46
- package/dist/components/core/carousel/grid/promotion/ui/CarouselGridPromotion.svelte +46 -28
- package/dist/components/core/carousel/highlights/ui/CarouselHighlights.svelte +57 -47
- package/dist/components/core/carousel/product/ui/CarouselProduct.svelte +57 -47
- package/dist/components/core/carousel/promotion/types.d.ts +5 -0
- package/dist/components/core/carousel/promotion/ui/CarouselPromotion.svelte +67 -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 +13 -9
- package/dist/components/pages/company-profile/data.js +47 -0
- package/dist/components/pages/company-profile/shared/ProfileBanner.svelte +136 -60
- 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 +1 -1
- package/dist/components/pages/company-profile/types.d.ts +25 -5
- 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 +60 -0
- package/dist/i18n/langs/en.d.ts +15 -0
- package/dist/i18n/langs/en.js +15 -0
- package/dist/i18n/langs/pt.d.ts +15 -0
- package/dist/i18n/langs/pt.js +15 -0
- package/dist/i18n/translations.d.ts +30 -0
- package/package.json +1 -1
|
@@ -1,47 +1,67 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import CardPromotion from "../../../card/promotion/CardPromotion.svelte";
|
|
3
|
-
import CarouselSlot from "../../../panel/CarouselSlot.svelte";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
{#
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import CardPromotion from "../../../card/promotion/CardPromotion.svelte";
|
|
3
|
+
import CarouselSlot from "../../../panel/CarouselSlot.svelte";
|
|
4
|
+
import NotFoundEmpty from "../../../panel/NotFoundEmpty.svelte";
|
|
5
|
+
import * as Carousel from "../../../../ui/carousel/index.js";
|
|
6
|
+
import type { CarouselPromotionProps } from "../types";
|
|
7
|
+
import CarouselHeader from "../../shared/ui/CarouselHeader.svelte";
|
|
8
|
+
import { useDevice } from "../../../../../hooks/responsive.svelte";
|
|
9
|
+
import { t } from "../../../../../i18n";
|
|
10
|
+
import Autoplay from "embla-carousel-autoplay";
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
headerProps,
|
|
14
|
+
slotProps,
|
|
15
|
+
items,
|
|
16
|
+
isLoading = false,
|
|
17
|
+
isDescriptionIcon,
|
|
18
|
+
isDescriptionLabel,
|
|
19
|
+
onClickBuy,
|
|
20
|
+
onClickShop,
|
|
21
|
+
onClickFavorite,
|
|
22
|
+
}: CarouselPromotionProps = $props();
|
|
23
|
+
|
|
24
|
+
const responsive = useDevice();
|
|
25
|
+
const isEmpty = $derived(!isLoading && (!items || items.length === 0));
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<CarouselHeader {...headerProps}>
|
|
29
|
+
{#if isEmpty}
|
|
30
|
+
<NotFoundEmpty
|
|
31
|
+
title={$t("empty.promotions.title")}
|
|
32
|
+
description={$t("empty.promotions.description")}
|
|
33
|
+
/>
|
|
34
|
+
{:else}
|
|
35
|
+
<CarouselSlot
|
|
36
|
+
{...slotProps}
|
|
37
|
+
containerClass="w-full"
|
|
38
|
+
plugins={[Autoplay({ delay: 4000, stopOnInteraction: true })]}
|
|
39
|
+
>
|
|
40
|
+
{#if isLoading}
|
|
41
|
+
{#each Array.from( { length: responsive.isMobile ? 5 : 10 }, ) as _, i (`loading-${i}`)}
|
|
42
|
+
<Carousel.Item class="pl-5 w-75 basis-auto">
|
|
43
|
+
<CardPromotion
|
|
44
|
+
id={i}
|
|
45
|
+
{isLoading}
|
|
46
|
+
{isDescriptionIcon}
|
|
47
|
+
{isDescriptionLabel}
|
|
48
|
+
/>
|
|
49
|
+
</Carousel.Item>
|
|
50
|
+
{/each}
|
|
51
|
+
{:else}
|
|
52
|
+
{#each items as item, i (`promotion-${item.id ?? i}`)}
|
|
53
|
+
<Carousel.Item class="pl-2 w-75 basis-auto">
|
|
54
|
+
<CardPromotion
|
|
55
|
+
{...item}
|
|
56
|
+
{isDescriptionIcon}
|
|
57
|
+
{isDescriptionLabel}
|
|
58
|
+
{onClickBuy}
|
|
59
|
+
{onClickShop}
|
|
60
|
+
{onClickFavorite}
|
|
61
|
+
/>
|
|
62
|
+
</Carousel.Item>
|
|
63
|
+
{/each}
|
|
64
|
+
{/if}
|
|
65
|
+
</CarouselSlot>
|
|
66
|
+
{/if}
|
|
67
|
+
</CarouselHeader>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Empty from "../../ui/empty/index.js";
|
|
3
|
+
import { t } from "../../../i18n";
|
|
4
|
+
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
5
|
+
import { Search01Icon } from "@hugeicons/core-free-icons";
|
|
6
|
+
import type { Snippet } from "svelte";
|
|
7
|
+
import type { IconSvgElement } from "@hugeicons/svelte";
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
icon?: IconSvgElement;
|
|
13
|
+
className?: string;
|
|
14
|
+
actionHref?: string;
|
|
15
|
+
actionLabel?: string;
|
|
16
|
+
onAction?: () => void;
|
|
17
|
+
showAction?: boolean;
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
title,
|
|
23
|
+
description,
|
|
24
|
+
icon = Search01Icon,
|
|
25
|
+
className = "",
|
|
26
|
+
actionHref = "#/",
|
|
27
|
+
actionLabel,
|
|
28
|
+
onAction,
|
|
29
|
+
showAction = false,
|
|
30
|
+
children,
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
const resolvedTitle = $derived(title ?? $t("empty.title"));
|
|
34
|
+
const resolvedDescription = $derived(description ?? $t("empty.description"));
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<Empty.Root class="w-full py-8 {className}">
|
|
38
|
+
<Empty.Header>
|
|
39
|
+
{#if icon}
|
|
40
|
+
<Empty.Media variant="icon">
|
|
41
|
+
<HugeiconsIcon {icon} class="size-6 text-muted-foreground" />
|
|
42
|
+
</Empty.Media>
|
|
43
|
+
{/if}
|
|
44
|
+
<Empty.Title class="text-base font-semibold">
|
|
45
|
+
{resolvedTitle}
|
|
46
|
+
</Empty.Title>
|
|
47
|
+
<Empty.Description class="text-sm text-muted-foreground max-w-sm">
|
|
48
|
+
{resolvedDescription}
|
|
49
|
+
</Empty.Description>
|
|
50
|
+
</Empty.Header>
|
|
51
|
+
|
|
52
|
+
{#if children}
|
|
53
|
+
<Empty.Content>
|
|
54
|
+
{@render children()}
|
|
55
|
+
</Empty.Content>
|
|
56
|
+
{:else if showAction || onAction}
|
|
57
|
+
<Empty.Content>
|
|
58
|
+
{#if onAction}
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onclick={onAction}
|
|
62
|
+
class="text-xs font-medium text-primary hover:underline"
|
|
63
|
+
>
|
|
64
|
+
{actionLabel ?? $t("empty.contact_support")}
|
|
65
|
+
</button>
|
|
66
|
+
{:else}
|
|
67
|
+
<Empty.Description class="text-xs">
|
|
68
|
+
{$t("empty.support")}
|
|
69
|
+
<a href={actionHref} class="text-primary hover:underline font-medium ml-1">
|
|
70
|
+
{actionLabel ?? $t("empty.contact_support")}
|
|
71
|
+
</a>
|
|
72
|
+
</Empty.Description>
|
|
73
|
+
{/if}
|
|
74
|
+
</Empty.Content>
|
|
75
|
+
{/if}
|
|
76
|
+
</Empty.Root>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { IconSvgElement } from "@hugeicons/svelte";
|
|
3
|
+
type Props = {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
icon?: IconSvgElement;
|
|
7
|
+
className?: string;
|
|
8
|
+
actionHref?: string;
|
|
9
|
+
actionLabel?: string;
|
|
10
|
+
onAction?: () => void;
|
|
11
|
+
showAction?: boolean;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
};
|
|
14
|
+
declare const NotFoundEmpty: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type NotFoundEmpty = ReturnType<typeof NotFoundEmpty>;
|
|
16
|
+
export default NotFoundEmpty;
|
|
@@ -27,16 +27,13 @@
|
|
|
27
27
|
import ProfileMenuBadge from "./shared/ProfileMenuBadge.svelte";
|
|
28
28
|
import ProfileAboutCompany from "./shared/ProfileAboutCompany.svelte";
|
|
29
29
|
import ProfileGallery from "./shared/ProfileGallery.svelte";
|
|
30
|
+
import ProfileProducts from "./shared/ProfileProducts.svelte";
|
|
30
31
|
import { defaultCompanyProfileData } from "./data";
|
|
31
32
|
import type { CompanyProfileProps, ProfileNavItem } from "./types";
|
|
32
33
|
|
|
33
34
|
let {
|
|
34
35
|
data = defaultCompanyProfileData,
|
|
35
36
|
isLoading = false,
|
|
36
|
-
yearFounded,
|
|
37
|
-
category,
|
|
38
|
-
address,
|
|
39
|
-
website,
|
|
40
37
|
onEmail,
|
|
41
38
|
onWhatsapp,
|
|
42
39
|
onCopyLink,
|
|
@@ -47,8 +44,9 @@
|
|
|
47
44
|
|
|
48
45
|
const sections: ProfileNavItem[] = [
|
|
49
46
|
{ id: "banner", label: "Apresentação" },
|
|
50
|
-
{ id: "sobre-empresa", label: "Sobre
|
|
51
|
-
{ id: "galeria-empresa", label: "Galeria
|
|
47
|
+
{ id: "sobre-empresa", label: "Sobre" },
|
|
48
|
+
{ id: "galeria-empresa", label: "Galeria" },
|
|
49
|
+
{ id: "products-empresa", label: "Produtos" },
|
|
52
50
|
];
|
|
53
51
|
|
|
54
52
|
let activeSection = $state(sections[0]?.id);
|
|
@@ -100,7 +98,7 @@
|
|
|
100
98
|
/>
|
|
101
99
|
{/if}
|
|
102
100
|
|
|
103
|
-
<div class="flex min-w-0 flex-1 flex-col gap-6 md:px-5 md:pl-10 lg:pl-12">
|
|
101
|
+
<div class="flex min-w-0 flex-1 flex-col gap-6 md:px-5 md:pl-10 lg:pl-12 pb-5 md:pb-10">
|
|
104
102
|
<div
|
|
105
103
|
id="banner"
|
|
106
104
|
bind:this={sectionEls["banner"]}
|
|
@@ -126,8 +124,6 @@
|
|
|
126
124
|
<ProfileAboutCompany data={data.about} {isLoading} />
|
|
127
125
|
</div>
|
|
128
126
|
|
|
129
|
-
<hr />
|
|
130
|
-
|
|
131
127
|
<div
|
|
132
128
|
id="galeria-empresa"
|
|
133
129
|
bind:this={sectionEls["galeria-empresa"]}
|
|
@@ -135,6 +131,14 @@
|
|
|
135
131
|
>
|
|
136
132
|
<ProfileGallery data={data.gallery} {isLoading} />
|
|
137
133
|
</div>
|
|
134
|
+
|
|
135
|
+
<div
|
|
136
|
+
id="products-empresa"
|
|
137
|
+
bind:this={sectionEls["products-empresa"]}
|
|
138
|
+
data-section-id="products-empresa"
|
|
139
|
+
>
|
|
140
|
+
<ProfileProducts data={data.products} {isLoading} />
|
|
141
|
+
</div>
|
|
138
142
|
</div>
|
|
139
143
|
</div>
|
|
140
144
|
</div>
|
|
@@ -65,4 +65,51 @@ export const defaultCompanyProfileData = {
|
|
|
65
65
|
},
|
|
66
66
|
],
|
|
67
67
|
},
|
|
68
|
+
products: {
|
|
69
|
+
title: "Produtos e Soluções",
|
|
70
|
+
description: "Conheça as principais soluções de embalagens e produtos industriais fabricados sob medida.",
|
|
71
|
+
variant: 1,
|
|
72
|
+
items: [
|
|
73
|
+
{
|
|
74
|
+
id: "prod-1",
|
|
75
|
+
title: "Caixas de Papelão Canelado",
|
|
76
|
+
content: "Embalagens resistentes de alta durabilidade para transporte industrial e exportação.",
|
|
77
|
+
price: "12.500 Kz",
|
|
78
|
+
imageUrl: "https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?q=80&w=600&auto=format&fit=crop",
|
|
79
|
+
buttonBuyText: "Solicitar Cotação",
|
|
80
|
+
onClickBuy: (id) => alert(`Solicitar produto ${id}`),
|
|
81
|
+
tags: [{ text: "Industrial" }, { text: "Canelado" }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "prod-2",
|
|
85
|
+
title: "Bobinas de Papel Kraft",
|
|
86
|
+
content: "Bobinas industriais de papel kraft para empacotamento e proteção de mercadorias.",
|
|
87
|
+
price: "35.000 Kz",
|
|
88
|
+
imageUrl: "https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?q=80&w=600&auto=format&fit=crop",
|
|
89
|
+
buttonBuyText: "Solicitar Cotação",
|
|
90
|
+
onClickBuy: (id) => alert(`Solicitar produto ${id}`),
|
|
91
|
+
tags: [{ text: "Kraft" }, { text: "Sustentável" }],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "prod-3",
|
|
95
|
+
title: "Caixas para Setor Alimentar",
|
|
96
|
+
content: "Embalagens certificadas para conservação e distribuição de alimentos frescos e congelados.",
|
|
97
|
+
price: "18.000 Kz",
|
|
98
|
+
imageUrl: "https://images.unsplash.com/photo-1504917599217-d4dc5ebe6122?q=80&w=600&auto=format&fit=crop",
|
|
99
|
+
buttonBuyText: "Solicitar Cotação",
|
|
100
|
+
onClickBuy: (id) => alert(`Solicitar produto ${id}`),
|
|
101
|
+
tags: [{ text: "Alimentar" }, { text: "Certificado" }],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "prod-4",
|
|
105
|
+
title: "Embalagens Personalizadas",
|
|
106
|
+
content: "Impressão flexográfica personalizada em caixas de todos os formatos e medidas.",
|
|
107
|
+
price: "22.000 Kz",
|
|
108
|
+
imageUrl: "https://images.unsplash.com/photo-1587293852726-70cdb56c2866?q=80&w=600&auto=format&fit=crop",
|
|
109
|
+
buttonBuyText: "Solicitar Cotação",
|
|
110
|
+
onClickBuy: (id) => alert(`Solicitar produto ${id}`),
|
|
111
|
+
tags: [{ text: "Personalizado" }, { text: "Design" }],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
68
115
|
};
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import ImgPlaceholder from "../../../../assets/placeholder-image.png";
|
|
19
19
|
import type { ProfileBannerData, ProfileBannerTag } from "../types";
|
|
20
20
|
import { HugeiconsIcon } from "@hugeicons/svelte";
|
|
21
|
+
import { onDestroy } from "svelte";
|
|
21
22
|
import {
|
|
22
23
|
Calendar01FreeIcons,
|
|
23
24
|
Copy,
|
|
@@ -29,6 +30,8 @@
|
|
|
29
30
|
WhatsappIcon,
|
|
30
31
|
} from "@hugeicons/core-free-icons";
|
|
31
32
|
|
|
33
|
+
const AUTOPLAY_MS = 8000;
|
|
34
|
+
|
|
32
35
|
let {
|
|
33
36
|
id,
|
|
34
37
|
data,
|
|
@@ -50,6 +53,76 @@
|
|
|
50
53
|
} = $props();
|
|
51
54
|
|
|
52
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
|
+
]);
|
|
53
126
|
</script>
|
|
54
127
|
|
|
55
128
|
{#if isLoading}
|
|
@@ -68,33 +141,58 @@
|
|
|
68
141
|
</div>
|
|
69
142
|
{:else}
|
|
70
143
|
<div
|
|
71
|
-
class="flex flex-col items-center justify-center
|
|
144
|
+
class="flex flex-col items-center justify-center p-2 relative rounded-lg overflow-hidden
|
|
145
|
+
{isCover ? '' : 'bg-gradient'}"
|
|
72
146
|
>
|
|
73
|
-
|
|
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
|
+
|
|
74
180
|
<div class="overflow-hidden rounded-xl absolute top-0 right-0">
|
|
75
181
|
<div class="flex gap-2 md:gap-5 p-4">
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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>
|
|
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}
|
|
94
193
|
</div>
|
|
95
194
|
</div>
|
|
96
195
|
|
|
97
|
-
<!-- Logótipo + nome + contacto -->
|
|
98
196
|
<div
|
|
99
197
|
class="flex flex-col items-center gap-3 px-4 text-center justify-center md:-mb-10"
|
|
100
198
|
>
|
|
@@ -103,7 +201,7 @@
|
|
|
103
201
|
<img
|
|
104
202
|
src={data.logo}
|
|
105
203
|
alt={data.name}
|
|
106
|
-
class="rounded-full w-15 md:w-25 h-15 md:h-25 p-2"
|
|
204
|
+
class="rounded-full w-15 md:w-25 h-15 md:h-25 lg:w-40 lg:h-40 p-2"
|
|
107
205
|
onerror={(e) =>
|
|
108
206
|
((e.target as HTMLImageElement).src = ImgPlaceholder)}
|
|
109
207
|
/>
|
|
@@ -117,27 +215,22 @@
|
|
|
117
215
|
{data.name}
|
|
118
216
|
</h1>
|
|
119
217
|
</div>
|
|
218
|
+
|
|
120
219
|
<div class="flex justify-between w-full p-5">
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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>
|
|
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}
|
|
139
231
|
</div>
|
|
140
232
|
</div>
|
|
233
|
+
|
|
141
234
|
<div
|
|
142
235
|
class="bg-gray-50 dark:bg-background p-2 md:p-10 flex flex-col items-center justify-center"
|
|
143
236
|
>
|
|
@@ -151,26 +244,9 @@
|
|
|
151
244
|
*:w-full
|
|
152
245
|
*:flex *:flex-col *:items-center"
|
|
153
246
|
>
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
})}
|
|
247
|
+
{#each infoItems as item (item.title)}
|
|
248
|
+
{@render companyInfo(item)}
|
|
249
|
+
{/each}
|
|
174
250
|
</div>
|
|
175
251
|
</div>
|
|
176
252
|
{/if}
|
|
@@ -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;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
<nav aria-label="Navegação do perfil" class="w-40 shrink-0 {className ?? ''}">
|
|
34
34
|
<div
|
|
35
|
-
class="fixed flex flex-col gap-1 md:gap-2 bg-card p-6 md:p-8 h-screen border-r"
|
|
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
36
|
>
|
|
37
37
|
{#if isLoading}
|
|
38
38
|
{#each Array.from({ length: 4 }) as _, i (i)}
|