@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.
Files changed (60) hide show
  1. package/README.md +93 -0
  2. package/package.json +57 -0
  3. package/src/astro-modules.d.ts +11 -0
  4. package/src/blocks/About.astro +94 -0
  5. package/src/blocks/Audience.astro +66 -0
  6. package/src/blocks/BlockRenderer.astro +105 -0
  7. package/src/blocks/Contact.astro +58 -0
  8. package/src/blocks/ContactDetails.astro +91 -0
  9. package/src/blocks/CtaBand.astro +53 -0
  10. package/src/blocks/EmergencyGrid.astro +47 -0
  11. package/src/blocks/EmergencyHelplines.astro +109 -0
  12. package/src/blocks/EmergencyPrimary.astro +51 -0
  13. package/src/blocks/Faq.astro +56 -0
  14. package/src/blocks/FeatureList.astro +73 -0
  15. package/src/blocks/Gallery.astro +95 -0
  16. package/src/blocks/Generic.astro +30 -0
  17. package/src/blocks/Hero.astro +82 -0
  18. package/src/blocks/IntroCta.astro +41 -0
  19. package/src/blocks/LegalDocument.astro +83 -0
  20. package/src/blocks/LocationsOverview.astro +161 -0
  21. package/src/blocks/NotFound.astro +33 -0
  22. package/src/blocks/Notice.astro +49 -0
  23. package/src/blocks/PageBanner.astro +57 -0
  24. package/src/blocks/Pricing.astro +97 -0
  25. package/src/blocks/ProfileDetail.astro +202 -0
  26. package/src/blocks/Projects.astro +91 -0
  27. package/src/blocks/Quote.astro +53 -0
  28. package/src/blocks/Services.astro +46 -0
  29. package/src/blocks/Skills.astro +65 -0
  30. package/src/blocks/Stats.astro +54 -0
  31. package/src/blocks/Steps.astro +76 -0
  32. package/src/blocks/TeamDirectory.astro +164 -0
  33. package/src/blocks/TeamTeaser.astro +84 -0
  34. package/src/blocks/Testimonials.astro +75 -0
  35. package/src/blocks/Timeline.astro +65 -0
  36. package/src/components/Actions.astro +67 -0
  37. package/src/components/Card.astro +104 -0
  38. package/src/components/Logo.astro +57 -0
  39. package/src/components/Media.astro +83 -0
  40. package/src/components/Prose.astro +29 -0
  41. package/src/components/ScrollToTop.astro +49 -0
  42. package/src/components/Section.astro +75 -0
  43. package/src/components/SectionHeader.astro +58 -0
  44. package/src/components/SectionHeading.astro +76 -0
  45. package/src/components/Seo.astro +55 -0
  46. package/src/components/SiteFooter.astro +90 -0
  47. package/src/components/SiteHeader.astro +97 -0
  48. package/src/config.ts +59 -0
  49. package/src/index.ts +75 -0
  50. package/src/layouts/Base.astro +57 -0
  51. package/src/lib/collections.ts +101 -0
  52. package/src/lib/content.ts +51 -0
  53. package/src/lib/format.ts +17 -0
  54. package/src/lib/markdown.ts +5 -0
  55. package/src/profiles/clinic.yaml +35 -0
  56. package/src/profiles/portfolio.yaml +27 -0
  57. package/src/profiles/therapist.yaml +31 -0
  58. package/src/scripts/enhancements.js +459 -0
  59. package/src/tokens/class-inventory.json +284 -0
  60. 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>