@sumaq/site-kit 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/package.json +57 -0
- package/src/astro-modules.d.ts +11 -0
- package/src/blocks/About.astro +94 -0
- package/src/blocks/Audience.astro +66 -0
- package/src/blocks/BlockRenderer.astro +105 -0
- package/src/blocks/Contact.astro +58 -0
- package/src/blocks/ContactDetails.astro +91 -0
- package/src/blocks/CtaBand.astro +53 -0
- package/src/blocks/EmergencyGrid.astro +47 -0
- package/src/blocks/EmergencyHelplines.astro +109 -0
- package/src/blocks/EmergencyPrimary.astro +51 -0
- package/src/blocks/Faq.astro +56 -0
- package/src/blocks/FeatureList.astro +73 -0
- package/src/blocks/Gallery.astro +95 -0
- package/src/blocks/Generic.astro +30 -0
- package/src/blocks/Hero.astro +82 -0
- package/src/blocks/IntroCta.astro +41 -0
- package/src/blocks/LegalDocument.astro +83 -0
- package/src/blocks/LocationsOverview.astro +161 -0
- package/src/blocks/NotFound.astro +33 -0
- package/src/blocks/Notice.astro +49 -0
- package/src/blocks/PageBanner.astro +57 -0
- package/src/blocks/Pricing.astro +97 -0
- package/src/blocks/ProfileDetail.astro +202 -0
- package/src/blocks/Projects.astro +91 -0
- package/src/blocks/Quote.astro +53 -0
- package/src/blocks/Services.astro +46 -0
- package/src/blocks/Skills.astro +65 -0
- package/src/blocks/Stats.astro +54 -0
- package/src/blocks/Steps.astro +76 -0
- package/src/blocks/TeamDirectory.astro +164 -0
- package/src/blocks/TeamTeaser.astro +84 -0
- package/src/blocks/Testimonials.astro +75 -0
- package/src/blocks/Timeline.astro +65 -0
- package/src/components/Actions.astro +67 -0
- package/src/components/Card.astro +104 -0
- package/src/components/Logo.astro +57 -0
- package/src/components/Media.astro +83 -0
- package/src/components/Prose.astro +29 -0
- package/src/components/ScrollToTop.astro +49 -0
- package/src/components/Section.astro +75 -0
- package/src/components/SectionHeader.astro +58 -0
- package/src/components/SectionHeading.astro +76 -0
- package/src/components/Seo.astro +55 -0
- package/src/components/SiteFooter.astro +90 -0
- package/src/components/SiteHeader.astro +97 -0
- package/src/config.ts +59 -0
- package/src/index.ts +75 -0
- package/src/layouts/Base.astro +57 -0
- package/src/lib/collections.ts +101 -0
- package/src/lib/content.ts +51 -0
- package/src/lib/format.ts +17 -0
- package/src/lib/markdown.ts +5 -0
- package/src/profiles/clinic.yaml +35 -0
- package/src/profiles/portfolio.yaml +27 -0
- package/src/profiles/therapist.yaml +31 -0
- package/src/scripts/enhancements.js +459 -0
- package/src/tokens/class-inventory.json +284 -0
- package/src/tokens/index.css +83 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Section from "../components/Section.astro";
|
|
3
|
+
import SectionHeader from "../components/SectionHeader.astro";
|
|
4
|
+
import Media from "../components/Media.astro";
|
|
5
|
+
import { cmsKey, hasAny, hasText, list } from "../lib/content";
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
sectionLabel,
|
|
9
|
+
sectionTitle,
|
|
10
|
+
sectionTitleAccent,
|
|
11
|
+
sectionSubtitle,
|
|
12
|
+
locations = [],
|
|
13
|
+
/**
|
|
14
|
+
* Label of the "see the rooms" link. The German default is the pre-0.2
|
|
15
|
+
* behaviour kept so the live site does not lose the link; move it to
|
|
16
|
+
* content and drop the fallback.
|
|
17
|
+
*/
|
|
18
|
+
roomsCtaLabel = "Räume ansehen",
|
|
19
|
+
section = "locationsOverview",
|
|
20
|
+
variant = "light",
|
|
21
|
+
id = "locations",
|
|
22
|
+
} = Astro.props;
|
|
23
|
+
|
|
24
|
+
const cms = section;
|
|
25
|
+
const cards = list<any>(locations).filter((location) => hasAny(location.name, location.images));
|
|
26
|
+
const hasTitle = hasText(sectionTitle) || hasText(sectionTitleAccent);
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<Section
|
|
30
|
+
name="locationsOverview"
|
|
31
|
+
variant={variant}
|
|
32
|
+
id={id}
|
|
33
|
+
labelledby={hasTitle ? "locations-title" : undefined}
|
|
34
|
+
cms={cms}
|
|
35
|
+
>
|
|
36
|
+
<SectionHeader
|
|
37
|
+
label={sectionLabel}
|
|
38
|
+
title={sectionTitle}
|
|
39
|
+
titleAccent={sectionTitleAccent}
|
|
40
|
+
lede={sectionSubtitle}
|
|
41
|
+
titleId="locations-title"
|
|
42
|
+
cms={cms}
|
|
43
|
+
/>
|
|
44
|
+
{
|
|
45
|
+
cards.length > 0 && (
|
|
46
|
+
<div class="sq-location__stack">
|
|
47
|
+
{cards.map((location: any, index: number) => {
|
|
48
|
+
const images = list<any>(location.images).filter((image) => hasText(image.src));
|
|
49
|
+
const features = list<any>(location.features).filter((feature) => hasText(feature.text));
|
|
50
|
+
const hasPrice = hasAny(location.priceLabel, location.priceValue);
|
|
51
|
+
const hasCta = hasText(location.href) && hasText(roomsCtaLabel);
|
|
52
|
+
return (
|
|
53
|
+
<article
|
|
54
|
+
class:list={["sq-location__card", location.reverse && "sq-location__card--reverse"]}
|
|
55
|
+
id={location.id}
|
|
56
|
+
>
|
|
57
|
+
{images.length > 0 && (
|
|
58
|
+
<div class="sq-location__gallery">
|
|
59
|
+
<div class="sq-location__main">
|
|
60
|
+
<Media
|
|
61
|
+
id={`${location.id}-main-img`}
|
|
62
|
+
src={images[0].src}
|
|
63
|
+
alt={images[0].alt}
|
|
64
|
+
cms={cmsKey(cms, "locations", index, "images", 0, "src")}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
{images.length > 1 && (
|
|
68
|
+
<div class="sq-location__thumbs" role="list">
|
|
69
|
+
{images.map((image: any, imageIndex: number) => (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class:list={["sq-location__thumb", imageIndex === 0 && "is-active"]}
|
|
73
|
+
data-target={`${location.id}-main-img`}
|
|
74
|
+
data-src={image.src}
|
|
75
|
+
data-alt={image.alt}
|
|
76
|
+
aria-label={`${image.alt || location.name} ${imageIndex + 1}`}
|
|
77
|
+
>
|
|
78
|
+
<Media
|
|
79
|
+
src={image.src}
|
|
80
|
+
alt=""
|
|
81
|
+
cms={cmsKey(cms, "locations", index, "images", imageIndex, "src")}
|
|
82
|
+
/>
|
|
83
|
+
</button>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
<span class="sq-location__index" aria-hidden="true">
|
|
88
|
+
{String(index + 1).padStart(2, "0")}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
<div class="sq-location__body">
|
|
93
|
+
{hasText(location.name) && (
|
|
94
|
+
<h3
|
|
95
|
+
class="sq-location__name"
|
|
96
|
+
data-location-name={location.id}
|
|
97
|
+
data-cms={cmsKey(cms, "locations", index, "name")}
|
|
98
|
+
>
|
|
99
|
+
{location.name}
|
|
100
|
+
</h3>
|
|
101
|
+
)}
|
|
102
|
+
{hasText(location.address) && (
|
|
103
|
+
<p
|
|
104
|
+
class="sq-location__address"
|
|
105
|
+
data-cms={cmsKey(cms, "locations", index, "address")}
|
|
106
|
+
>
|
|
107
|
+
{location.address}
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
{hasText(location.description) && (
|
|
111
|
+
<p
|
|
112
|
+
class="sq-location__desc"
|
|
113
|
+
data-cms={cmsKey(cms, "locations", index, "description")}
|
|
114
|
+
>
|
|
115
|
+
{location.description}
|
|
116
|
+
</p>
|
|
117
|
+
)}
|
|
118
|
+
{features.length > 0 && (
|
|
119
|
+
<ul class="sq-feature-list">
|
|
120
|
+
{features.map((feature: any, fi: number) => (
|
|
121
|
+
<li data-cms={cmsKey(cms, "locations", index, "features", fi, "text")}>
|
|
122
|
+
{feature.text}
|
|
123
|
+
</li>
|
|
124
|
+
))}
|
|
125
|
+
</ul>
|
|
126
|
+
)}
|
|
127
|
+
{(hasPrice || hasCta) && (
|
|
128
|
+
<div class="sq-location__foot">
|
|
129
|
+
{hasPrice && (
|
|
130
|
+
<p
|
|
131
|
+
class="sq-pricing__tag"
|
|
132
|
+
data-cms={cmsKey(cms, "locations", index, "priceLabel")}
|
|
133
|
+
>
|
|
134
|
+
{location.priceLabel}{" "}
|
|
135
|
+
{hasText(location.priceValue) && (
|
|
136
|
+
<strong data-cms={cmsKey(cms, "locations", index, "priceValue")}>
|
|
137
|
+
{location.priceValue}
|
|
138
|
+
</strong>
|
|
139
|
+
)}
|
|
140
|
+
{hasText(location.priceSuffix) ? ` ${location.priceSuffix}` : ""}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
{hasCta && (
|
|
144
|
+
<a
|
|
145
|
+
class="sq-btn sq-btn--ghost sq-btn--sm"
|
|
146
|
+
href={location.href}
|
|
147
|
+
data-rooms-cta={location.id}
|
|
148
|
+
>
|
|
149
|
+
{roomsCtaLabel}
|
|
150
|
+
</a>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</article>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
</Section>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 404 body. Extracted from `pages/404.astro` (www-juliaschaffer-at) — the
|
|
4
|
+
* only site of the five that ships one, though every site needs it.
|
|
5
|
+
*/
|
|
6
|
+
import Section from "../components/Section.astro";
|
|
7
|
+
import Actions from "../components/Actions.astro";
|
|
8
|
+
import { cmsKey, hasText } from "../lib/content";
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
code = "404",
|
|
12
|
+
title,
|
|
13
|
+
body,
|
|
14
|
+
ctas = [],
|
|
15
|
+
section = "notFound",
|
|
16
|
+
variant = "light",
|
|
17
|
+
} = Astro.props;
|
|
18
|
+
|
|
19
|
+
const cms = section;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<Section name="not-found" variant={variant} container="narrow" containerClass="sq-center" cms={cms}>
|
|
23
|
+
{hasText(code) && <p class="sq-not-found__code" data-cms={cmsKey(cms, "code")}>{code}</p>}
|
|
24
|
+
{
|
|
25
|
+
hasText(title) && (
|
|
26
|
+
<h1 class="sq-section__title" data-cms={cmsKey(cms, "title")}>
|
|
27
|
+
{title}
|
|
28
|
+
</h1>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
{hasText(body) && <p class="sq-section__lede" data-cms={cmsKey(cms, "body")}>{body}</p>}
|
|
32
|
+
<Actions ctas={ctas} cms={cmsKey(cms, "ctas")} />
|
|
33
|
+
</Section>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Callout / advisory box. Extracted from the `alert alert-warning` in
|
|
4
|
+
* `Contact.astro` (www-utehofer-at) and the highlighted `info.box` in
|
|
5
|
+
* `Services.astro` (www-bettinageidl-at).
|
|
6
|
+
*
|
|
7
|
+
* `tone` is a declared variant, not a colour: the site decides what
|
|
8
|
+
* `sq-notice--warning` looks like.
|
|
9
|
+
*/
|
|
10
|
+
import Prose from "../components/Prose.astro";
|
|
11
|
+
import { cmsKey, hasAny, hasText } from "../lib/content";
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
title,
|
|
15
|
+
body,
|
|
16
|
+
tone = "info",
|
|
17
|
+
/** Wrap in its own `<section>`; off when placed inside another block. */
|
|
18
|
+
standalone = false,
|
|
19
|
+
section = "notice",
|
|
20
|
+
id,
|
|
21
|
+
} = Astro.props;
|
|
22
|
+
|
|
23
|
+
const cms = section;
|
|
24
|
+
const role = tone === "warning" || tone === "danger" ? "alert" : "note";
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
hasAny(title, body) && (
|
|
29
|
+
<aside
|
|
30
|
+
class:list={[
|
|
31
|
+
"sq-notice",
|
|
32
|
+
`sq-notice--${tone}`,
|
|
33
|
+
standalone && "sq-section",
|
|
34
|
+
]}
|
|
35
|
+
id={id}
|
|
36
|
+
role={role}
|
|
37
|
+
data-cms={cms}
|
|
38
|
+
>
|
|
39
|
+
<div class:list={[standalone && "sq-container sq-container--narrow"]}>
|
|
40
|
+
{hasText(title) && (
|
|
41
|
+
<p class="sq-notice__title" data-cms={cmsKey(cms, "title")}>
|
|
42
|
+
{title}
|
|
43
|
+
</p>
|
|
44
|
+
)}
|
|
45
|
+
<Prose body={body} class="sq-notice__body" cms={cmsKey(cms, "body")} />
|
|
46
|
+
</div>
|
|
47
|
+
</aside>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { cmsKey, hasAny, hasText } from "../lib/content";
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
eyebrow,
|
|
6
|
+
title,
|
|
7
|
+
titleAccent,
|
|
8
|
+
sectionSubtitle,
|
|
9
|
+
bannerLead,
|
|
10
|
+
emergency = false,
|
|
11
|
+
transitionName,
|
|
12
|
+
section,
|
|
13
|
+
} = Astro.props;
|
|
14
|
+
const lead = hasText(bannerLead) ? bannerLead : sectionSubtitle;
|
|
15
|
+
const cms = section ?? "pageBanner";
|
|
16
|
+
const hasTitle = hasText(title) || hasText(titleAccent);
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
hasAny(eyebrow, title, titleAccent, lead) && (
|
|
21
|
+
<header class="sq-banner" data-cms={cms}>
|
|
22
|
+
<div class="sq-banner__overlay" aria-hidden="true" />
|
|
23
|
+
<div class="sq-container sq-banner__content">
|
|
24
|
+
{hasText(eyebrow) && (
|
|
25
|
+
<p
|
|
26
|
+
class:list={["sq-eyebrow", emergency && "sq-eyebrow--emergency"]}
|
|
27
|
+
data-cms={cmsKey(cms, "eyebrow")}
|
|
28
|
+
>
|
|
29
|
+
{eyebrow}
|
|
30
|
+
</p>
|
|
31
|
+
)}
|
|
32
|
+
{hasTitle && (
|
|
33
|
+
<h1
|
|
34
|
+
class:list={["sq-banner__title", emergency && "sq-emergency__headline"]}
|
|
35
|
+
transition:name={transitionName}
|
|
36
|
+
data-cms={cmsKey(cms, "title")}
|
|
37
|
+
>
|
|
38
|
+
{title}
|
|
39
|
+
{hasText(titleAccent) && (
|
|
40
|
+
<span class="sq-accent" data-cms={cmsKey(cms, "titleAccent")}>
|
|
41
|
+
{titleAccent}
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
</h1>
|
|
45
|
+
)}
|
|
46
|
+
{hasText(lead) && (
|
|
47
|
+
<p
|
|
48
|
+
class="sq-banner__lede"
|
|
49
|
+
data-cms={cmsKey(cms, hasText(bannerLead) ? "bannerLead" : "sectionSubtitle")}
|
|
50
|
+
>
|
|
51
|
+
{lead}
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Section from "../components/Section.astro";
|
|
3
|
+
import SectionHeader from "../components/SectionHeader.astro";
|
|
4
|
+
import { cmsKey, hasText, list } from "../lib/content";
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
sectionLabel,
|
|
8
|
+
sectionTitle,
|
|
9
|
+
sectionTitleAccent,
|
|
10
|
+
sectionSubtitle,
|
|
11
|
+
plans = [],
|
|
12
|
+
section = "pricing",
|
|
13
|
+
variant = "muted",
|
|
14
|
+
id = "pricing",
|
|
15
|
+
} = Astro.props;
|
|
16
|
+
|
|
17
|
+
const cms = section;
|
|
18
|
+
const cards = list<any>(plans).filter((plan) => hasText(plan.name) || list(plan.items).length > 0);
|
|
19
|
+
const hasTitle = hasText(sectionTitle) || hasText(sectionTitleAccent);
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<Section
|
|
23
|
+
name="pricing"
|
|
24
|
+
variant={variant}
|
|
25
|
+
id={id}
|
|
26
|
+
labelledby={hasTitle ? "pricing-title" : undefined}
|
|
27
|
+
cms={cms}
|
|
28
|
+
>
|
|
29
|
+
<SectionHeader
|
|
30
|
+
label={sectionLabel}
|
|
31
|
+
title={sectionTitle}
|
|
32
|
+
titleAccent={sectionTitleAccent}
|
|
33
|
+
lede={sectionSubtitle}
|
|
34
|
+
titleId="pricing-title"
|
|
35
|
+
cms={cms}
|
|
36
|
+
/>
|
|
37
|
+
{
|
|
38
|
+
cards.length > 0 && (
|
|
39
|
+
<div class="sq-pricing__grid">
|
|
40
|
+
{cards.map((plan: any, i: number) => {
|
|
41
|
+
const rows = list<any>(plan.items).filter(
|
|
42
|
+
(item) => hasText(item.label) || hasText(item.price),
|
|
43
|
+
);
|
|
44
|
+
return (
|
|
45
|
+
<article class:list={["sq-pricing__card", plan.featured && "sq-pricing__card--featured"]}>
|
|
46
|
+
{hasText(plan.badge) && (
|
|
47
|
+
<p class="sq-pricing__badge" data-cms={cmsKey(cms, "plans", i, "badge")}>
|
|
48
|
+
{plan.badge}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
{hasText(plan.name) && (
|
|
52
|
+
<h3 data-cms={cmsKey(cms, "plans", i, "name")}>{plan.name}</h3>
|
|
53
|
+
)}
|
|
54
|
+
{hasText(plan.description) && (
|
|
55
|
+
<p class="sq-pricing__desc" data-cms={cmsKey(cms, "plans", i, "description")}>
|
|
56
|
+
{plan.description}
|
|
57
|
+
</p>
|
|
58
|
+
)}
|
|
59
|
+
{rows.length > 0 && (
|
|
60
|
+
<ul class="sq-pricing__list">
|
|
61
|
+
{rows.map((item: any, ii: number) => (
|
|
62
|
+
<li>
|
|
63
|
+
{hasText(item.label) && (
|
|
64
|
+
<span data-cms={cmsKey(cms, "plans", i, "items", ii, "label")}>
|
|
65
|
+
{item.label}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
{hasText(item.price) && (
|
|
69
|
+
<strong data-cms={cmsKey(cms, "plans", i, "items", ii, "price")}>
|
|
70
|
+
{item.price}
|
|
71
|
+
</strong>
|
|
72
|
+
)}
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>
|
|
76
|
+
)}
|
|
77
|
+
{hasText(plan.note) && (
|
|
78
|
+
<p class="sq-section__note" data-cms={cmsKey(cms, "plans", i, "note")}>
|
|
79
|
+
{plan.note}
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
{hasText(plan.ctaHref) && hasText(plan.ctaLabel) && (
|
|
83
|
+
<a
|
|
84
|
+
class="sq-btn sq-btn--primary sq-btn--block"
|
|
85
|
+
href={plan.ctaHref}
|
|
86
|
+
data-cms={cmsKey(cms, "plans", i, "ctaLabel")}
|
|
87
|
+
>
|
|
88
|
+
{plan.ctaLabel}
|
|
89
|
+
</a>
|
|
90
|
+
)}
|
|
91
|
+
</article>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
</Section>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Detail page for one collection entry (a team member).
|
|
4
|
+
*
|
|
5
|
+
* Lifted out of `pages/team/[slug].astro` in www-therapieraum-graz-at, where
|
|
6
|
+
* it was 130 lines of markup living in a page. The page now only resolves the
|
|
7
|
+
* entry and hands it over, which is what the site-kit spec asks of pages.
|
|
8
|
+
*/
|
|
9
|
+
import Section from "../components/Section.astro";
|
|
10
|
+
import Media from "../components/Media.astro";
|
|
11
|
+
import Prose from "../components/Prose.astro";
|
|
12
|
+
import { telHref } from "../lib/format";
|
|
13
|
+
import { cmsKey, hasAny, hasImage, hasText, list } from "../lib/content";
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
name,
|
|
17
|
+
role,
|
|
18
|
+
photo,
|
|
19
|
+
quote,
|
|
20
|
+
quoteCite,
|
|
21
|
+
body,
|
|
22
|
+
/** `[{ label, value, href?, type? }]` — email, phone, website, free rows. */
|
|
23
|
+
info = [],
|
|
24
|
+
/** `[{ title, items: [{ text }], modifier? }]` — rendered as tag clouds. */
|
|
25
|
+
tagSections = [],
|
|
26
|
+
/** `[{ title, items: [{ period?, text }] }]` — rendered as timelines. */
|
|
27
|
+
timelineSections = [],
|
|
28
|
+
backLabel,
|
|
29
|
+
backHref,
|
|
30
|
+
transitionName,
|
|
31
|
+
section = "profile",
|
|
32
|
+
variant = "light",
|
|
33
|
+
} = Astro.props;
|
|
34
|
+
|
|
35
|
+
const cms = section;
|
|
36
|
+
|
|
37
|
+
const hrefFor = (row: any): string | undefined => {
|
|
38
|
+
if (hasText(row.href)) return row.href;
|
|
39
|
+
if (!hasText(row.value)) return undefined;
|
|
40
|
+
if (row.type === "email") return `mailto:${row.value}`;
|
|
41
|
+
if (row.type === "phone") return telHref(row.value);
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const rows = list<any>(info)
|
|
46
|
+
.filter((row) => hasText(row.value))
|
|
47
|
+
.map((row) => ({ ...row, resolvedHref: hrefFor(row) }));
|
|
48
|
+
|
|
49
|
+
const tags = list<any>(tagSections)
|
|
50
|
+
.map((part) => ({
|
|
51
|
+
...part,
|
|
52
|
+
entries: list<any>(part.items)
|
|
53
|
+
.map((item) => (typeof item === "string" ? item : item.text))
|
|
54
|
+
.filter(hasText),
|
|
55
|
+
}))
|
|
56
|
+
.filter((part) => part.entries.length > 0);
|
|
57
|
+
|
|
58
|
+
const timelines = list<any>(timelineSections)
|
|
59
|
+
.map((part) => ({
|
|
60
|
+
...part,
|
|
61
|
+
entries: list<any>(part.items).filter((item) => hasText(item.text)),
|
|
62
|
+
}))
|
|
63
|
+
.filter((part) => part.entries.length > 0);
|
|
64
|
+
|
|
65
|
+
const hasSidebar = hasImage(photo) || hasAny(name, role) || rows.length > 0;
|
|
66
|
+
const hasMain = hasAny(quote, body) || tags.length > 0 || timelines.length > 0;
|
|
67
|
+
const showBack = hasText(backHref) && hasText(backLabel);
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
<Section name="profile" variant={variant} containerClass="sq-profile__grid" cms={cms}>
|
|
71
|
+
{
|
|
72
|
+
hasSidebar && (
|
|
73
|
+
<aside class="sq-profile__sidebar">
|
|
74
|
+
{hasImage(photo) && (
|
|
75
|
+
<div class="sq-profile__photo">
|
|
76
|
+
<Media
|
|
77
|
+
src={photo}
|
|
78
|
+
alt={photo?.alt || name}
|
|
79
|
+
width={640}
|
|
80
|
+
height={640}
|
|
81
|
+
transition:name={transitionName}
|
|
82
|
+
cms={cmsKey(cms, "photo", "src")}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
{hasText(name) && (
|
|
87
|
+
<h2 class="sq-profile__sidebar-name" data-cms={cmsKey(cms, "name")}>
|
|
88
|
+
{name}
|
|
89
|
+
</h2>
|
|
90
|
+
)}
|
|
91
|
+
{hasText(role) && (
|
|
92
|
+
<p class="sq-profile__sidebar-title" data-cms={cmsKey(cms, "role")}>
|
|
93
|
+
{role}
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
{rows.length > 0 && (
|
|
97
|
+
<ul class="sq-profile__info-list">
|
|
98
|
+
{rows.map((row: any, i: number) => (
|
|
99
|
+
<li>
|
|
100
|
+
{hasText(row.label) && (
|
|
101
|
+
<span class="sq-profile__info-label" data-cms={cmsKey(cms, "info", i, "label")}>
|
|
102
|
+
{row.label}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
<span class="sq-profile__info-value">
|
|
106
|
+
{row.resolvedHref ? (
|
|
107
|
+
<a
|
|
108
|
+
href={row.resolvedHref}
|
|
109
|
+
target={row.external ? "_blank" : undefined}
|
|
110
|
+
rel={row.external ? "noopener" : undefined}
|
|
111
|
+
data-cms={cmsKey(cms, "info", i, "value")}
|
|
112
|
+
>
|
|
113
|
+
{row.linkLabel || row.value}
|
|
114
|
+
</a>
|
|
115
|
+
) : (
|
|
116
|
+
<span data-cms={cmsKey(cms, "info", i, "value")}>{row.value}</span>
|
|
117
|
+
)}
|
|
118
|
+
</span>
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
)}
|
|
123
|
+
</aside>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
(hasMain || showBack) && (
|
|
129
|
+
<div class="sq-profile__main">
|
|
130
|
+
{hasText(quote) && (
|
|
131
|
+
<blockquote class="sq-profile__quote" data-cms={cmsKey(cms, "quote")}>
|
|
132
|
+
{quote}
|
|
133
|
+
{hasText(quoteCite) && <cite data-cms={cmsKey(cms, "quoteCite")}>{quoteCite}</cite>}
|
|
134
|
+
</blockquote>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{tags.map((part: any, i: number) => (
|
|
138
|
+
<div class="sq-profile__section">
|
|
139
|
+
{hasText(part.title) && (
|
|
140
|
+
<h2 class="sq-profile__section-title" data-cms={cmsKey(cms, "tagSections", i, "title")}>
|
|
141
|
+
{part.title}
|
|
142
|
+
</h2>
|
|
143
|
+
)}
|
|
144
|
+
<div class="sq-profile__tags">
|
|
145
|
+
{part.entries.map((entry: string, ei: number) => (
|
|
146
|
+
<span
|
|
147
|
+
class:list={["sq-tag", part.modifier]}
|
|
148
|
+
data-cms={cmsKey(cms, "tagSections", i, "items", ei, "text")}
|
|
149
|
+
>
|
|
150
|
+
{entry}
|
|
151
|
+
</span>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
|
|
157
|
+
{hasText(body) && (
|
|
158
|
+
<div class="sq-profile__section">
|
|
159
|
+
<Prose body={body} cms={cmsKey(cms, "body")} />
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{timelines.map((part: any, i: number) => (
|
|
164
|
+
<div class="sq-profile__section">
|
|
165
|
+
{hasText(part.title) && (
|
|
166
|
+
<h2
|
|
167
|
+
class="sq-profile__section-title"
|
|
168
|
+
data-cms={cmsKey(cms, "timelineSections", i, "title")}
|
|
169
|
+
>
|
|
170
|
+
{part.title}
|
|
171
|
+
</h2>
|
|
172
|
+
)}
|
|
173
|
+
<div class="sq-profile__timeline">
|
|
174
|
+
{part.entries.map((item: any, ei: number) => (
|
|
175
|
+
<div
|
|
176
|
+
class="sq-profile__timeline-item"
|
|
177
|
+
data-cms={cmsKey(cms, "timelineSections", i, "items", ei, "text")}
|
|
178
|
+
>
|
|
179
|
+
{hasText(item.period) ? (
|
|
180
|
+
<>
|
|
181
|
+
<strong>{item.period}:</strong> {item.text}
|
|
182
|
+
</>
|
|
183
|
+
) : (
|
|
184
|
+
item.text
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
|
|
192
|
+
{showBack && (
|
|
193
|
+
<div class="sq-profile__return">
|
|
194
|
+
<a class="sq-btn sq-btn--ghost" href={backHref}>
|
|
195
|
+
{backLabel}
|
|
196
|
+
</a>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
</Section>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Portfolio grid. Extracted from `Projects.astro` (www-elingan-dev).
|
|
4
|
+
*/
|
|
5
|
+
import Section from "../components/Section.astro";
|
|
6
|
+
import SectionHeader from "../components/SectionHeader.astro";
|
|
7
|
+
import Media from "../components/Media.astro";
|
|
8
|
+
import { cmsKey, hasImage, hasText, list } from "../lib/content";
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
sectionLabel,
|
|
12
|
+
sectionTitle,
|
|
13
|
+
sectionTitleAccent,
|
|
14
|
+
sectionSubtitle,
|
|
15
|
+
items = [],
|
|
16
|
+
section = "projects",
|
|
17
|
+
variant = "light",
|
|
18
|
+
id = "projects",
|
|
19
|
+
} = Astro.props;
|
|
20
|
+
|
|
21
|
+
const cms = section;
|
|
22
|
+
const projects = list<any>(items).filter((item) => hasText(item.name));
|
|
23
|
+
const isExternal = (url?: string) => hasText(url) && /^https?:\/\//.test(url!);
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<Section name="projects" variant={variant} id={id} cms={cms}>
|
|
27
|
+
<SectionHeader
|
|
28
|
+
label={sectionLabel}
|
|
29
|
+
title={sectionTitle}
|
|
30
|
+
titleAccent={sectionTitleAccent}
|
|
31
|
+
lede={sectionSubtitle}
|
|
32
|
+
cms={cms}
|
|
33
|
+
/>
|
|
34
|
+
{
|
|
35
|
+
projects.length > 0 && (
|
|
36
|
+
<ul class="sq-projects__body" role="list">
|
|
37
|
+
{projects.map((project: any, i: number) => {
|
|
38
|
+
const tags = list<any>(project.tags)
|
|
39
|
+
.map((tag) => (typeof tag === "string" ? tag : tag.name))
|
|
40
|
+
.filter(hasText);
|
|
41
|
+
const linked = hasText(project.url);
|
|
42
|
+
const Wrapper = linked ? "a" : "div";
|
|
43
|
+
return (
|
|
44
|
+
<li class="sq-projects__item">
|
|
45
|
+
<Wrapper
|
|
46
|
+
class="sq-projects__link"
|
|
47
|
+
href={linked ? project.url : undefined}
|
|
48
|
+
target={isExternal(project.url) ? "_blank" : undefined}
|
|
49
|
+
rel={isExternal(project.url) ? "noopener" : undefined}
|
|
50
|
+
>
|
|
51
|
+
{hasImage(project.image) && (
|
|
52
|
+
<div class="sq-projects__media">
|
|
53
|
+
<Media
|
|
54
|
+
src={project.image}
|
|
55
|
+
alt={project.image?.alt || ""}
|
|
56
|
+
width={800}
|
|
57
|
+
height={500}
|
|
58
|
+
cms={cmsKey(cms, "items", i, "image", "src")}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
<div class="sq-projects__details">
|
|
63
|
+
<h3 class="sq-projects__name" data-cms={cmsKey(cms, "items", i, "name")}>
|
|
64
|
+
{project.name}
|
|
65
|
+
</h3>
|
|
66
|
+
{hasText(project.description) && (
|
|
67
|
+
<p
|
|
68
|
+
class="sq-projects__desc"
|
|
69
|
+
data-cms={cmsKey(cms, "items", i, "description")}
|
|
70
|
+
>
|
|
71
|
+
{project.description}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
{tags.length > 0 && (
|
|
75
|
+
<ul class="sq-projects__tags" role="list">
|
|
76
|
+
{tags.map((tag, ti) => (
|
|
77
|
+
<li class="sq-tag" data-cms={cmsKey(cms, "items", i, "tags", ti, "name")}>
|
|
78
|
+
{tag}
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</Wrapper>
|
|
85
|
+
</li>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</ul>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
</Section>
|