@press2ai/theme-therapy-soft 0.1.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 +19 -0
- package/package.json +29 -0
- package/src/index.ts +2 -0
- package/src/styles/base.ts +86 -0
- package/src/styles/catalog-grid.ts +21 -0
- package/src/styles/category-nav.ts +12 -0
- package/src/styles/footer.ts +29 -0
- package/src/styles/forms.ts +20 -0
- package/src/styles/header.ts +16 -0
- package/src/styles/hero.ts +68 -0
- package/src/styles/index.ts +32 -0
- package/src/styles/pagination.ts +13 -0
- package/src/styles/profile-article.ts +56 -0
- package/src/styles/profile-card.ts +57 -0
- package/src/styles/stat-bar.ts +31 -0
- package/src/templates/catalog.ts +77 -0
- package/src/templates/helpers.ts +22 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/layout.ts +61 -0
- package/src/templates/profile-article.ts +85 -0
- package/src/templates/profile-card.ts +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @press2ai/theme-therapy-soft
|
|
2
|
+
|
|
3
|
+
Warm classless theme for therapy verticals (`otwarty-terapeuta`).
|
|
4
|
+
|
|
5
|
+
Sage greens + warm beiges, Crimson Text serif headings, Inter body. Structurally compatible with `@press2ai/engine` `TrenerTheme` interface — drop-in replacement for `@press2ai/theme-specialist-glossy` in CEIDG verticals.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
layout, profileCard, profileArticle, esc,
|
|
12
|
+
catalogHero, catalogGrid, statBar, categoryNav, pagination,
|
|
13
|
+
} from '@press2ai/theme-therapy-soft/templates';
|
|
14
|
+
import { composeCss } from '@press2ai/theme-therapy-soft/styles';
|
|
15
|
+
|
|
16
|
+
const CSS_TAG = `<style>${composeCss('hero', 'statBar', 'categoryNav', 'catalogGrid', 'profileCard', 'profileArticle', 'pagination', 'forms')}</style>`;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Pair with `createVerticalApp` from `@press2ai/engine/ceidg-vertical`.
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@press2ai/theme-therapy-soft",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Warm sage/beige classless theme for therapy verticals. Crimson Text serif + Inter. Structurally compatible with @press2ai/engine TrenerTheme.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://www.npmjs.com/package/@press2ai/theme-therapy-soft",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"hono",
|
|
10
|
+
"cloudflare-workers",
|
|
11
|
+
"classless",
|
|
12
|
+
"semantic",
|
|
13
|
+
"therapy",
|
|
14
|
+
"schema-org",
|
|
15
|
+
"press2ai"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.ts",
|
|
26
|
+
"./templates": "./src/templates/index.ts",
|
|
27
|
+
"./styles": "./src/styles/index.ts"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const css = `:root {
|
|
2
|
+
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
|
|
3
|
+
--font-serif: 'Crimson Text', Georgia, 'Times New Roman', serif;
|
|
4
|
+
|
|
5
|
+
--g1: 0.5rem; --g2: 0.8125rem; --g3: 1.3125rem; --g4: 2.125rem; --g5: 3.4375rem; --g6: 5.5625rem; --g7: 9rem;
|
|
6
|
+
--t-xs: 0.8125rem; --t-sm: 1rem; --t-md: 1.25rem; --t-lg: 1.625rem; --t-xl: 2.125rem; --t-2xl: 2.875rem; --t-3xl: 3.75rem;
|
|
7
|
+
--container: 1180px; --pad: var(--g4);
|
|
8
|
+
--w-6: 520px; --w-8: 720px; --w-10: 880px; --measure: 38rem;
|
|
9
|
+
|
|
10
|
+
--sage-50: #f7f5f2;
|
|
11
|
+
--sage-100: #efece7;
|
|
12
|
+
--sage-200: #dde2dd;
|
|
13
|
+
--sage-400: #a8baa8;
|
|
14
|
+
--sage-500: #8ea78e;
|
|
15
|
+
--sage-600: #7a8f7a;
|
|
16
|
+
--sage-700: #6a7f6a;
|
|
17
|
+
--sage-800: #4f5f4f;
|
|
18
|
+
|
|
19
|
+
--beige-50: #faf5ef;
|
|
20
|
+
--beige-100: #f3ece4;
|
|
21
|
+
--beige-200: #e8ddd0;
|
|
22
|
+
|
|
23
|
+
--stone-300: #d6d3d1;
|
|
24
|
+
--stone-400: #a8a29e;
|
|
25
|
+
--stone-500: #78716c;
|
|
26
|
+
--stone-600: #57534e;
|
|
27
|
+
--stone-700: #44403c;
|
|
28
|
+
--stone-900: #1c1917;
|
|
29
|
+
|
|
30
|
+
--emerald-100: #d1fae5;
|
|
31
|
+
--emerald-700: #047857;
|
|
32
|
+
--amber-100: #fef3c7;
|
|
33
|
+
--amber-700: #b45309;
|
|
34
|
+
|
|
35
|
+
--bg: var(--sage-50);
|
|
36
|
+
--card: var(--beige-50);
|
|
37
|
+
--ink: var(--stone-900);
|
|
38
|
+
--ink-2: var(--stone-600);
|
|
39
|
+
--ink-3: var(--stone-500);
|
|
40
|
+
--line: var(--stone-300);
|
|
41
|
+
--line-soft: var(--beige-200);
|
|
42
|
+
--accent: var(--sage-600);
|
|
43
|
+
--accent-h: var(--sage-700);
|
|
44
|
+
--accent-soft: var(--sage-100);
|
|
45
|
+
|
|
46
|
+
--r: 4px; --r-lg: 8px; --pill: 999px;
|
|
47
|
+
--shadow: 0 1px 2px rgba(60,50,40,.04);
|
|
48
|
+
--shadow-md: 0 4px 16px rgba(60,50,40,.06), 0 1px 3px rgba(60,50,40,.04);
|
|
49
|
+
--shadow-up: 0 12px 28px rgba(60,50,40,.10), 0 2px 6px rgba(60,50,40,.04);
|
|
50
|
+
--ease: 220ms cubic-bezier(.4,0,.2,1);
|
|
51
|
+
}
|
|
52
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; }
|
|
53
|
+
html { font-size: 16px; -webkit-font-smoothing: antialiased; scroll-behavior: smooth; }
|
|
54
|
+
body {
|
|
55
|
+
font-family: var(--font-sans); font-weight: 400;
|
|
56
|
+
color: var(--ink); background: var(--bg); line-height: 1.65; overflow-x: clip;
|
|
57
|
+
}
|
|
58
|
+
h1, h2, h3, h4 {
|
|
59
|
+
color: var(--ink); font-family: var(--font-serif); font-weight: 600;
|
|
60
|
+
letter-spacing: -0.01em; line-height: 1.15;
|
|
61
|
+
}
|
|
62
|
+
h1 { font-size: clamp(var(--t-2xl), 5.5vw, var(--t-3xl)); margin-bottom: var(--g3); }
|
|
63
|
+
h2 { font-size: clamp(var(--t-lg), 3vw, var(--t-xl)); margin-bottom: var(--g2); }
|
|
64
|
+
h3 { font-size: var(--t-md); font-weight: 600; line-height: 1.35; margin-bottom: var(--g2); }
|
|
65
|
+
h4 { font-size: var(--t-sm); font-weight: 600; }
|
|
66
|
+
p {
|
|
67
|
+
font-size: var(--t-sm); line-height: 1.7; color: var(--ink-2);
|
|
68
|
+
margin-bottom: var(--g2); max-width: var(--measure);
|
|
69
|
+
}
|
|
70
|
+
a { color: var(--accent-h); text-decoration: none; transition: color var(--ease); }
|
|
71
|
+
a:hover { color: var(--ink); }
|
|
72
|
+
small { font-size: var(--t-xs); color: var(--ink-3); }
|
|
73
|
+
strong { color: var(--ink); font-weight: 600; }
|
|
74
|
+
main {
|
|
75
|
+
max-width: var(--container); margin-inline: auto;
|
|
76
|
+
padding-inline: var(--pad); padding-top: 0; padding-bottom: 0;
|
|
77
|
+
}
|
|
78
|
+
main:not(:has(> section, > article, > div, > nav)) {
|
|
79
|
+
padding-top: var(--g5); padding-bottom: var(--g5);
|
|
80
|
+
}
|
|
81
|
+
main > * + * { margin-top: var(--g6); }
|
|
82
|
+
@media (max-width: 640px) {
|
|
83
|
+
:root { --pad: var(--g3); }
|
|
84
|
+
main { padding: 0 var(--pad) var(--g4); }
|
|
85
|
+
main > * + * { margin-top: var(--g5); }
|
|
86
|
+
}`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const css = `section:has(> article[itemscope]) {
|
|
2
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
3
|
+
gap: var(--g3);
|
|
4
|
+
}
|
|
5
|
+
section:has(> article[itemscope]) > h2 {
|
|
6
|
+
grid-column: 1 / -1; text-align: center; margin-bottom: var(--g4);
|
|
7
|
+
font-family: var(--font-serif); font-weight: 400;
|
|
8
|
+
font-size: clamp(var(--t-lg), 3vw, var(--t-xl));
|
|
9
|
+
}
|
|
10
|
+
section:has(> article[itemscope]) > h2::before {
|
|
11
|
+
content: 'Nasi terapeuci'; display: block;
|
|
12
|
+
font-family: var(--font-sans); font-size: var(--t-xs); font-weight: 500;
|
|
13
|
+
color: var(--sage-700); letter-spacing: 0.1em; text-transform: uppercase;
|
|
14
|
+
margin-bottom: var(--g2);
|
|
15
|
+
}
|
|
16
|
+
section:has(> article[itemscope]) > nav[aria-label] {
|
|
17
|
+
grid-column: 1 / -1; margin-bottom: var(--g3);
|
|
18
|
+
}
|
|
19
|
+
@media (max-width: 640px) {
|
|
20
|
+
section:has(> article[itemscope]) { grid-template-columns: 1fr; }
|
|
21
|
+
}`;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const css = `nav[aria-label] {
|
|
2
|
+
display: flex; flex-wrap: wrap; gap: var(--g1); justify-content: center;
|
|
3
|
+
}
|
|
4
|
+
nav[aria-label] a {
|
|
5
|
+
display: inline-flex; align-items: center; height: 40px; padding: 0 var(--g3);
|
|
6
|
+
font-size: var(--t-xs); font-weight: 500;
|
|
7
|
+
background: var(--beige-50); border: 1px solid var(--line);
|
|
8
|
+
color: var(--ink-2); transition: all var(--ease);
|
|
9
|
+
}
|
|
10
|
+
nav[aria-label] a:hover {
|
|
11
|
+
background: var(--sage-100); border-color: var(--sage-400); color: var(--sage-700);
|
|
12
|
+
}`;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const css = `body > footer {
|
|
2
|
+
border-top: 1px solid var(--line-soft); background: var(--beige-50);
|
|
3
|
+
padding: var(--g6) 0 var(--g4); margin-top: var(--g6);
|
|
4
|
+
}
|
|
5
|
+
body > footer > small {
|
|
6
|
+
display: block; max-width: var(--container); margin-inline: auto; padding-inline: var(--pad);
|
|
7
|
+
color: var(--ink-3); font-size: var(--t-xs);
|
|
8
|
+
}
|
|
9
|
+
body > footer a { color: var(--ink-2); transition: color var(--ease); }
|
|
10
|
+
body > footer a:hover { color: var(--accent-h); }
|
|
11
|
+
body > footer > small > nav {
|
|
12
|
+
display: grid; grid-template-columns: 2fr 1fr 1fr; gap: var(--g5);
|
|
13
|
+
margin-bottom: var(--g5); padding-bottom: var(--g4);
|
|
14
|
+
border-bottom: 1px solid var(--line-soft);
|
|
15
|
+
}
|
|
16
|
+
body > footer > small > nav section { all: unset; display: block; }
|
|
17
|
+
body > footer > small > nav strong {
|
|
18
|
+
display: block; font-family: var(--font-sans); font-size: var(--t-xs);
|
|
19
|
+
font-weight: 600; color: var(--ink); margin-bottom: var(--g2);
|
|
20
|
+
}
|
|
21
|
+
body > footer > small > nav p { font-size: var(--t-xs); line-height: 1.7; color: var(--ink-3); margin: 0; max-width: 36ch; }
|
|
22
|
+
body > footer > small > nav a { display: block; font-size: var(--t-xs); line-height: 2; color: var(--ink-2); }
|
|
23
|
+
body > footer > small > nav a:hover { color: var(--accent-h); }
|
|
24
|
+
body > footer > small > p {
|
|
25
|
+
font-size: var(--t-xs); color: var(--ink-3); text-align: left;
|
|
26
|
+
}
|
|
27
|
+
@media (max-width: 640px) {
|
|
28
|
+
body > footer > small > nav { grid-template-columns: 1fr; gap: var(--g3); }
|
|
29
|
+
}`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const css = `form:not([role="search"]) label {
|
|
2
|
+
display: block; font-size: var(--t-sm); color: var(--ink-2);
|
|
3
|
+
font-weight: 500; margin-bottom: var(--g1); font-family: var(--font-sans);
|
|
4
|
+
}
|
|
5
|
+
form:not([role="search"]) input[type="text"] {
|
|
6
|
+
display: block; width: 100%; margin-top: var(--g1); height: 48px;
|
|
7
|
+
padding: 0 var(--g3); background: var(--beige-50); border: 1px solid var(--line);
|
|
8
|
+
color: var(--ink); font-size: var(--t-sm); font-family: var(--font-sans);
|
|
9
|
+
transition: border-color var(--ease);
|
|
10
|
+
}
|
|
11
|
+
form:not([role="search"]) input[type="text"]:focus {
|
|
12
|
+
outline: none; border-color: var(--sage-600);
|
|
13
|
+
}
|
|
14
|
+
form:not([role="search"]) button[type="submit"] {
|
|
15
|
+
margin-top: var(--g3); height: 48px; padding: 0 var(--g4);
|
|
16
|
+
border: none; background: var(--sage-600); color: var(--beige-50);
|
|
17
|
+
font-size: var(--t-sm); font-weight: 500; font-family: var(--font-sans);
|
|
18
|
+
cursor: pointer; transition: background var(--ease);
|
|
19
|
+
}
|
|
20
|
+
form:not([role="search"]) button[type="submit"]:hover { background: var(--sage-700); }`;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const css = `body > header {
|
|
2
|
+
background: var(--beige-50); border-bottom: 1px solid var(--line-soft);
|
|
3
|
+
}
|
|
4
|
+
body > header nav {
|
|
5
|
+
max-width: var(--container); margin-inline: auto; padding-inline: var(--pad);
|
|
6
|
+
height: 80px; display: flex; align-items: center; justify-content: space-between;
|
|
7
|
+
}
|
|
8
|
+
body > header nav a {
|
|
9
|
+
color: var(--ink-2); text-decoration: none; font-size: var(--t-xs);
|
|
10
|
+
font-weight: 500; transition: color var(--ease);
|
|
11
|
+
}
|
|
12
|
+
body > header nav a:hover { color: var(--ink); }
|
|
13
|
+
body > header nav a strong {
|
|
14
|
+
font-family: var(--font-serif); font-size: var(--t-md); font-weight: 600;
|
|
15
|
+
color: var(--ink); letter-spacing: 0;
|
|
16
|
+
}`;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const css = `hgroup {
|
|
2
|
+
position: relative; text-align: center; isolation: isolate;
|
|
3
|
+
padding: var(--g7) var(--pad) var(--g6);
|
|
4
|
+
margin-inline: calc(50% - 50vw);
|
|
5
|
+
background: var(--sage-100);
|
|
6
|
+
border-bottom: 1px solid var(--line-soft);
|
|
7
|
+
}
|
|
8
|
+
hgroup > * { position: relative; max-width: var(--w-10); margin-inline: auto; }
|
|
9
|
+
hgroup > p:first-child {
|
|
10
|
+
display: inline-flex; margin: 0 auto var(--g3); padding: 0;
|
|
11
|
+
}
|
|
12
|
+
hgroup > p:first-child small {
|
|
13
|
+
display: inline-flex; align-items: center; gap: var(--g1);
|
|
14
|
+
background: var(--beige-50); padding: var(--g1) var(--g2);
|
|
15
|
+
border: 1px solid var(--sage-200); border-radius: var(--pill);
|
|
16
|
+
font-size: var(--t-xs); font-weight: 500; color: var(--sage-700);
|
|
17
|
+
letter-spacing: 0.04em; text-transform: uppercase;
|
|
18
|
+
}
|
|
19
|
+
hgroup > p:first-child small::before {
|
|
20
|
+
content: ''; width: 6px; height: 6px; border-radius: 50%;
|
|
21
|
+
background: var(--sage-500); box-shadow: 0 0 0 3px rgba(142,167,142,.25);
|
|
22
|
+
}
|
|
23
|
+
hgroup h1 {
|
|
24
|
+
font-family: var(--font-serif); font-weight: 400;
|
|
25
|
+
max-width: 22ch; margin: 0 auto var(--g4); color: var(--stone-900);
|
|
26
|
+
letter-spacing: -0.015em;
|
|
27
|
+
}
|
|
28
|
+
hgroup > p:not(:first-child) {
|
|
29
|
+
font-family: var(--font-sans); font-size: var(--t-md); font-weight: 400;
|
|
30
|
+
line-height: 1.6; color: var(--ink-2); max-width: var(--measure);
|
|
31
|
+
margin: 0 auto var(--g4);
|
|
32
|
+
}
|
|
33
|
+
hgroup > nav {
|
|
34
|
+
display: flex; gap: var(--g2); flex-wrap: wrap; justify-content: center;
|
|
35
|
+
margin-top: var(--g4); margin-bottom: 0;
|
|
36
|
+
}
|
|
37
|
+
hgroup > nav > a {
|
|
38
|
+
display: inline-flex; align-items: center; height: 52px; padding: 0 var(--g4);
|
|
39
|
+
font-weight: 500; font-size: var(--t-sm); transition: all var(--ease);
|
|
40
|
+
background: var(--beige-50); color: var(--ink); border: 1px solid var(--line);
|
|
41
|
+
}
|
|
42
|
+
hgroup > nav > a:first-child {
|
|
43
|
+
background: var(--sage-600); color: var(--beige-50); border-color: transparent;
|
|
44
|
+
}
|
|
45
|
+
hgroup > nav > a:first-child:hover { background: var(--sage-700); }
|
|
46
|
+
hgroup > nav > a:not(:first-child):hover { background: var(--beige-100); }
|
|
47
|
+
form[role="search"] {
|
|
48
|
+
display: flex; max-width: var(--w-6); height: 52px;
|
|
49
|
+
margin: var(--g4) auto 0; background: var(--beige-50);
|
|
50
|
+
border: 1px solid var(--line); overflow: hidden; transition: all var(--ease);
|
|
51
|
+
}
|
|
52
|
+
form[role="search"]:focus-within { border-color: var(--sage-600); }
|
|
53
|
+
form[role="search"] input {
|
|
54
|
+
flex: 1; min-width: 0; border: none; background: transparent; color: var(--ink);
|
|
55
|
+
padding: 0 var(--g3); font-size: var(--t-sm); font-family: var(--font-sans); outline: none;
|
|
56
|
+
-webkit-appearance: none; appearance: none;
|
|
57
|
+
}
|
|
58
|
+
form[role="search"] input::placeholder { color: var(--ink-3); }
|
|
59
|
+
form[role="search"] button {
|
|
60
|
+
flex-shrink: 0; height: 100%; border: none; background: var(--sage-600); color: var(--beige-50);
|
|
61
|
+
padding: 0 var(--g4); font-size: var(--t-sm); font-weight: 500;
|
|
62
|
+
font-family: var(--font-sans); cursor: pointer; transition: background var(--ease);
|
|
63
|
+
}
|
|
64
|
+
form[role="search"] button:hover { background: var(--sage-700); }
|
|
65
|
+
@media (max-width: 640px) {
|
|
66
|
+
hgroup { padding: var(--g5) var(--pad) var(--g4); }
|
|
67
|
+
hgroup > nav > a, form[role="search"] { height: 44px; }
|
|
68
|
+
}`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { css as base } from './base.ts';
|
|
2
|
+
import { css as header } from './header.ts';
|
|
3
|
+
import { css as footer } from './footer.ts';
|
|
4
|
+
import { css as hero } from './hero.ts';
|
|
5
|
+
import { css as statBar } from './stat-bar.ts';
|
|
6
|
+
import { css as categoryNav } from './category-nav.ts';
|
|
7
|
+
import { css as catalogGrid } from './catalog-grid.ts';
|
|
8
|
+
import { css as profileCard } from './profile-card.ts';
|
|
9
|
+
import { css as profileArticle } from './profile-article.ts';
|
|
10
|
+
import { css as pagination } from './pagination.ts';
|
|
11
|
+
import { css as forms } from './forms.ts';
|
|
12
|
+
|
|
13
|
+
const components: Record<string, string> = {
|
|
14
|
+
hero,
|
|
15
|
+
statBar,
|
|
16
|
+
categoryNav,
|
|
17
|
+
catalogGrid,
|
|
18
|
+
profileCard,
|
|
19
|
+
profileArticle,
|
|
20
|
+
pagination,
|
|
21
|
+
forms,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const layoutCss = base + '\n' + header + '\n' + footer;
|
|
25
|
+
|
|
26
|
+
export function composeCss(...names: string[]): string {
|
|
27
|
+
return layoutCss + '\n' + names.map(n => components[n] ?? '').join('\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const allCss = layoutCss + '\n' + Object.values(components).join('\n');
|
|
31
|
+
|
|
32
|
+
export { base, header, footer, hero, statBar, categoryNav, catalogGrid, profileCard, profileArticle, pagination, forms };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const css = `main > nav p {
|
|
2
|
+
display: flex; align-items: center; justify-content: center; gap: var(--g3);
|
|
3
|
+
font-size: var(--t-xs); color: var(--ink-3); font-family: var(--font-sans);
|
|
4
|
+
}
|
|
5
|
+
main > nav a {
|
|
6
|
+
height: 40px; display: inline-flex; align-items: center; padding: 0 var(--g3);
|
|
7
|
+
background: var(--beige-50); border: 1px solid var(--line);
|
|
8
|
+
font-size: var(--t-xs); font-weight: 500; color: var(--ink-2);
|
|
9
|
+
transition: all var(--ease);
|
|
10
|
+
}
|
|
11
|
+
main > nav a:hover {
|
|
12
|
+
background: var(--sage-100); border-color: var(--sage-400); color: var(--sage-700);
|
|
13
|
+
}`;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const css = `main > article {
|
|
2
|
+
max-width: var(--w-8); margin-inline: auto;
|
|
3
|
+
padding: var(--g6) 0;
|
|
4
|
+
}
|
|
5
|
+
main > article > header {
|
|
6
|
+
padding-bottom: var(--g4); margin-bottom: var(--g4);
|
|
7
|
+
border-bottom: 1px solid var(--line-soft);
|
|
8
|
+
}
|
|
9
|
+
main > article > header h1 {
|
|
10
|
+
font-family: var(--font-serif); font-weight: 400;
|
|
11
|
+
font-size: clamp(var(--t-xl), 5vw, var(--t-2xl));
|
|
12
|
+
letter-spacing: -0.01em; line-height: 1.1; margin-bottom: var(--g1);
|
|
13
|
+
}
|
|
14
|
+
main > article > header p {
|
|
15
|
+
font-size: var(--t-md); color: var(--sage-700); margin: 0; font-weight: 400;
|
|
16
|
+
font-style: italic; font-family: var(--font-serif);
|
|
17
|
+
}
|
|
18
|
+
main > article address {
|
|
19
|
+
font-style: normal; font-size: var(--t-sm); color: var(--ink-2);
|
|
20
|
+
padding: var(--g2) 0; margin-bottom: var(--g3); border-bottom: 1px solid var(--line-soft);
|
|
21
|
+
}
|
|
22
|
+
main > article address strong { color: var(--ink-3); font-weight: 500; }
|
|
23
|
+
main > article section {
|
|
24
|
+
margin: var(--g4) 0; padding-top: var(--g4); border-top: 1px solid var(--line-soft);
|
|
25
|
+
}
|
|
26
|
+
main > article section h2 {
|
|
27
|
+
font-family: var(--font-sans); font-size: var(--t-xs); font-weight: 600;
|
|
28
|
+
text-transform: uppercase; letter-spacing: 0.1em; color: var(--sage-700);
|
|
29
|
+
margin: 0 0 var(--g3);
|
|
30
|
+
}
|
|
31
|
+
main > article section > p {
|
|
32
|
+
font-family: var(--font-serif); font-size: var(--t-md); line-height: 1.7;
|
|
33
|
+
color: var(--ink-2); max-width: none;
|
|
34
|
+
}
|
|
35
|
+
main > article ul {
|
|
36
|
+
list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: var(--g1);
|
|
37
|
+
}
|
|
38
|
+
main > article ul li {
|
|
39
|
+
font-size: var(--t-xs); font-weight: 500; color: var(--sage-800);
|
|
40
|
+
padding: var(--g1) var(--g2); background: var(--sage-100);
|
|
41
|
+
}
|
|
42
|
+
main > article dl {
|
|
43
|
+
display: grid; grid-template-columns: 9rem 1fr; gap: var(--g2) var(--g3);
|
|
44
|
+
}
|
|
45
|
+
main > article dt {
|
|
46
|
+
font-size: var(--t-xs); color: var(--ink-3); font-weight: 500;
|
|
47
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
48
|
+
}
|
|
49
|
+
main > article dd { margin: 0; font-size: var(--t-sm); color: var(--ink); }
|
|
50
|
+
main > article dd a { color: var(--sage-700); }
|
|
51
|
+
main > article dd a:hover { color: var(--ink); }
|
|
52
|
+
@media (max-width: 640px) {
|
|
53
|
+
main > article { padding: var(--g4) 0; }
|
|
54
|
+
main > article dl { grid-template-columns: 1fr; gap: 2px 0; }
|
|
55
|
+
main > article dt { margin-top: var(--g2); }
|
|
56
|
+
}`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const S = 'section:has(> article[itemscope]) > article[itemscope]';
|
|
2
|
+
export const css = `${S} {
|
|
3
|
+
position: relative; background: var(--beige-50); border: 1px solid var(--line-soft);
|
|
4
|
+
padding: var(--g3); display: flex; flex-direction: column;
|
|
5
|
+
box-shadow: var(--shadow); transition: all var(--ease);
|
|
6
|
+
}
|
|
7
|
+
${S}:hover {
|
|
8
|
+
border-color: var(--sage-400); box-shadow: var(--shadow-md);
|
|
9
|
+
}
|
|
10
|
+
${S} > div[aria-hidden] {
|
|
11
|
+
width: 56px; height: 56px; border-radius: 50%;
|
|
12
|
+
display: flex; align-items: center; justify-content: center;
|
|
13
|
+
font-family: var(--font-serif); font-size: var(--t-md); font-weight: 600;
|
|
14
|
+
color: var(--beige-50); background: var(--sage-600);
|
|
15
|
+
margin-bottom: var(--g3); flex-shrink: 0;
|
|
16
|
+
}
|
|
17
|
+
${S} header { margin-bottom: var(--g2); }
|
|
18
|
+
${S} h2 {
|
|
19
|
+
font-family: var(--font-serif); font-size: var(--t-md); font-weight: 600;
|
|
20
|
+
line-height: 1.25; margin: 0 0 var(--g1); letter-spacing: 0;
|
|
21
|
+
}
|
|
22
|
+
${S} h2 a { color: var(--ink); }
|
|
23
|
+
${S} h2 a:hover { color: var(--sage-700); }
|
|
24
|
+
${S} header p {
|
|
25
|
+
font-size: var(--t-xs); line-height: 1.5; color: var(--ink-3); margin: 0 0 2px;
|
|
26
|
+
}
|
|
27
|
+
${S} header p:has([itemprop="address"]) {
|
|
28
|
+
display: inline-flex; align-items: center; gap: 6px; margin-top: var(--g1);
|
|
29
|
+
color: var(--stone-500);
|
|
30
|
+
}
|
|
31
|
+
${S} header p:has([itemprop="address"])::before {
|
|
32
|
+
content: '\u00b7'; color: var(--sage-500); font-weight: 700;
|
|
33
|
+
}
|
|
34
|
+
${S} [itemprop="description"] {
|
|
35
|
+
font-size: var(--t-xs); line-height: 1.6; margin: var(--g2) 0 0; color: var(--ink-2);
|
|
36
|
+
}
|
|
37
|
+
${S} ul {
|
|
38
|
+
list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 4px;
|
|
39
|
+
margin: var(--g2) 0 0;
|
|
40
|
+
}
|
|
41
|
+
${S} ul li {
|
|
42
|
+
background: var(--sage-100); color: var(--sage-800);
|
|
43
|
+
padding: 3px var(--g1); font-size: 0.75rem; font-weight: 500;
|
|
44
|
+
}
|
|
45
|
+
${S} > a:last-child {
|
|
46
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
47
|
+
margin-top: auto; padding: var(--g2) 0;
|
|
48
|
+
background: var(--sage-600); color: var(--beige-50);
|
|
49
|
+
font-size: var(--t-xs); font-weight: 500;
|
|
50
|
+
margin-left: calc(-1 * var(--g3)); margin-right: calc(-1 * var(--g3));
|
|
51
|
+
margin-bottom: calc(-1 * var(--g3));
|
|
52
|
+
transition: background var(--ease);
|
|
53
|
+
}
|
|
54
|
+
${S} > article > a:last-child:hover,
|
|
55
|
+
${S} > a:last-child:hover { background: var(--sage-700); color: var(--beige-50); }
|
|
56
|
+
${S} > * + * { margin-top: 0; }
|
|
57
|
+
${S} > a:last-child { margin-top: var(--g3); }`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const css = `section[aria-label="Statystyki"] {
|
|
2
|
+
background: var(--beige-50); border-top: 1px solid var(--line-soft);
|
|
3
|
+
border-bottom: 1px solid var(--line-soft);
|
|
4
|
+
margin-inline: calc(50% - 50vw); padding: var(--g5) var(--pad);
|
|
5
|
+
}
|
|
6
|
+
section[aria-label="Statystyki"] > p {
|
|
7
|
+
text-align: center; font-size: var(--t-md); font-weight: 400; line-height: 1.6;
|
|
8
|
+
color: var(--ink); margin: 0 auto var(--g4); max-width: var(--measure);
|
|
9
|
+
font-family: var(--font-serif); font-style: italic;
|
|
10
|
+
}
|
|
11
|
+
section[aria-label="Statystyki"] > dl {
|
|
12
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
13
|
+
gap: var(--g4); max-width: var(--w-10); margin: 0 auto; padding: 0;
|
|
14
|
+
}
|
|
15
|
+
section[aria-label="Statystyki"] > dl div { text-align: center; }
|
|
16
|
+
section[aria-label="Statystyki"] > dl div > span:has(> svg) {
|
|
17
|
+
display: flex; align-items: center; justify-content: center; width: 44px; height: 44px;
|
|
18
|
+
margin: 0 auto var(--g2); color: var(--sage-600);
|
|
19
|
+
}
|
|
20
|
+
section[aria-label="Statystyki"] > dl div > span:has(> svg) > svg { width: 24px; height: 24px; }
|
|
21
|
+
section[aria-label="Statystyki"] > dl dt {
|
|
22
|
+
font-family: var(--font-serif); font-size: var(--t-2xl); font-weight: 400;
|
|
23
|
+
line-height: 1.1; color: var(--stone-900);
|
|
24
|
+
}
|
|
25
|
+
section[aria-label="Statystyki"] > dl dd {
|
|
26
|
+
margin: var(--g1) 0 0; font-size: var(--t-xs); line-height: 1.4;
|
|
27
|
+
color: var(--ink-3); font-weight: 400;
|
|
28
|
+
}
|
|
29
|
+
@media (max-width: 640px) {
|
|
30
|
+
section[aria-label="Statystyki"] > dl { grid-template-columns: repeat(2, 1fr); gap: var(--g3); }
|
|
31
|
+
}`;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { esc } from './helpers.ts';
|
|
2
|
+
|
|
3
|
+
export interface CatalogHeroProps {
|
|
4
|
+
badge?: string;
|
|
5
|
+
title: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
searchAction?: string;
|
|
8
|
+
searchPlaceholder?: string;
|
|
9
|
+
searchValue?: string;
|
|
10
|
+
ctas?: { label: string; href: string }[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function catalogHero(p: CatalogHeroProps): string {
|
|
14
|
+
const badge = p.badge ? `<p><small>${esc(p.badge)}</small></p>` : '';
|
|
15
|
+
const subtitle = p.subtitle ? `<p>${esc(p.subtitle)}</p>` : '';
|
|
16
|
+
const search = p.searchAction !== undefined ? `<form role="search" action="${esc(p.searchAction ?? '/')}" method="get">
|
|
17
|
+
<input type="search" name="q" placeholder="${esc(p.searchPlaceholder ?? 'Szukaj...')}" value="${esc(p.searchValue ?? '')}" />
|
|
18
|
+
<button type="submit">Szukaj</button>
|
|
19
|
+
</form>` : '';
|
|
20
|
+
const ctas = p.ctas?.length ? `<nav>${p.ctas.map(
|
|
21
|
+
c => `<a href="${esc(c.href)}">${esc(c.label)}</a>`
|
|
22
|
+
).join('\n')}</nav>` : '';
|
|
23
|
+
return `<hgroup>
|
|
24
|
+
${badge}
|
|
25
|
+
<h1>${p.title}</h1>
|
|
26
|
+
${subtitle}
|
|
27
|
+
${search}
|
|
28
|
+
${ctas}
|
|
29
|
+
</hgroup>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STAT_ICONS: Record<string, string> = {
|
|
33
|
+
people: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
|
|
34
|
+
city: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-4"/></svg>',
|
|
35
|
+
free: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function statBar(items: { value: string; label: string; icon?: string }[], summary?: string): string {
|
|
39
|
+
const divs = items.map(i => {
|
|
40
|
+
const icon = i.icon && STAT_ICONS[i.icon] ? `<span>${STAT_ICONS[i.icon]}</span>` : '';
|
|
41
|
+
return `<div>${icon}<dt>${esc(i.value)}</dt><dd>${esc(i.label)}</dd></div>`;
|
|
42
|
+
}).join('\n');
|
|
43
|
+
const summaryP = summary ? `<p>${esc(summary)}</p>` : '';
|
|
44
|
+
return `<section aria-label="Statystyki">${summaryP}<dl aria-hidden="true">\n${divs}\n</dl></section>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function categoryNav(items: { href: string; label: string }[], ariaLabel = 'Kategorie'): string {
|
|
48
|
+
if (!items.length) return '';
|
|
49
|
+
const links = items.map(i => `<a href="${esc(i.href)}">${esc(i.label)}</a>`).join('');
|
|
50
|
+
return `<nav aria-label="${esc(ariaLabel)}">${links}</nav>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function catalogGrid(p: { title: string; filters?: string; cards: string }): string {
|
|
54
|
+
return `<section>
|
|
55
|
+
<h2>${esc(p.title)}</h2>
|
|
56
|
+
${p.filters ?? ''}
|
|
57
|
+
${p.cards}
|
|
58
|
+
</section>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PaginationProps {
|
|
62
|
+
current: number;
|
|
63
|
+
total: number;
|
|
64
|
+
baseHref?: string;
|
|
65
|
+
extraParams?: string;
|
|
66
|
+
prevLabel?: string;
|
|
67
|
+
nextLabel?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function pagination(p: PaginationProps): string {
|
|
71
|
+
if (p.total <= 1) return '';
|
|
72
|
+
const base = p.baseHref ?? '/';
|
|
73
|
+
const extra = p.extraParams ?? '';
|
|
74
|
+
const prev = p.current > 1 ? `<a href="${esc(base)}?p=${p.current - 1}${extra}">${esc(p.prevLabel ?? '\u2190 Poprzednia')}</a>` : '';
|
|
75
|
+
const next = p.current < p.total ? `<a href="${esc(base)}?p=${p.current + 1}${extra}">${esc(p.nextLabel ?? 'Następna \u2192')}</a>` : '';
|
|
76
|
+
return `<nav><p>${prev} Strona ${p.current} z ${p.total} ${next}</p></nav>`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function esc(s: string | undefined | null): string {
|
|
2
|
+
if (!s) return '';
|
|
3
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface TherapyProfile {
|
|
7
|
+
slug: string;
|
|
8
|
+
firstName: string;
|
|
9
|
+
lastName: string;
|
|
10
|
+
jobTitle: string;
|
|
11
|
+
city?: string;
|
|
12
|
+
bio?: string;
|
|
13
|
+
photo?: string;
|
|
14
|
+
specialties: string[];
|
|
15
|
+
languages: string[];
|
|
16
|
+
business: { name?: string; taxId?: string; classification: string[] };
|
|
17
|
+
social: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function fullName(p: TherapyProfile): string {
|
|
21
|
+
return `${p.firstName} ${p.lastName}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { layout, type LayoutProps } from './layout.ts';
|
|
2
|
+
export { profileCard } from './profile-card.ts';
|
|
3
|
+
export { profileArticle } from './profile-article.ts';
|
|
4
|
+
export { catalogHero, catalogGrid, statBar, categoryNav, pagination, type CatalogHeroProps, type PaginationProps } from './catalog.ts';
|
|
5
|
+
export { esc, fullName, type TherapyProfile } from './helpers.ts';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { esc } from './helpers.ts';
|
|
2
|
+
|
|
3
|
+
export interface LayoutProps {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
jsonLd?: object;
|
|
7
|
+
siteName?: string;
|
|
8
|
+
homeHref?: string;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
headExtra?: string;
|
|
11
|
+
navExtra?: string;
|
|
12
|
+
footerContent?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const THEME_VERSION = '0.1.0';
|
|
16
|
+
|
|
17
|
+
export function layout(props: LayoutProps, body: string): string {
|
|
18
|
+
const {
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
jsonLd,
|
|
22
|
+
siteName = 'press2ai',
|
|
23
|
+
homeHref = '/',
|
|
24
|
+
headExtra = '',
|
|
25
|
+
navExtra = '',
|
|
26
|
+
footerContent,
|
|
27
|
+
} = props;
|
|
28
|
+
|
|
29
|
+
const footer = footerContent ??
|
|
30
|
+
`Powered by <a href="https://www.npmjs.com/org/press2ai">press2ai</a> ·
|
|
31
|
+
<a href="https://www.npmjs.com/package/@press2ai/theme-therapy-soft/v/${THEME_VERSION}">@press2ai/theme-therapy-soft v${THEME_VERSION}</a>`;
|
|
32
|
+
|
|
33
|
+
return `<!doctype html>
|
|
34
|
+
<html lang="pl">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="UTF-8">
|
|
37
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
38
|
+
<title>${esc(title)}</title>
|
|
39
|
+
${description ? `<meta name="description" content="${esc(description)}">` : ''}
|
|
40
|
+
<meta property="og:title" content="${esc(title)}">
|
|
41
|
+
${description ? `<meta property="og:description" content="${esc(description)}">` : ''}
|
|
42
|
+
${jsonLd ? `<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, '\\u003c')}</script>` : ''}
|
|
43
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
44
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
45
|
+
<link href="https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
46
|
+
${headExtra}
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<header>
|
|
50
|
+
<nav>
|
|
51
|
+
<a href="${esc(homeHref)}"><strong>${esc(siteName)}</strong></a>
|
|
52
|
+
${navExtra}
|
|
53
|
+
</nav>
|
|
54
|
+
</header>
|
|
55
|
+
<main>${body}</main>
|
|
56
|
+
<footer>
|
|
57
|
+
<small>${footer}</small>
|
|
58
|
+
</footer>
|
|
59
|
+
</body>
|
|
60
|
+
</html>`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { esc, fullName, type TherapyProfile } from './helpers.ts';
|
|
2
|
+
|
|
3
|
+
export function profileArticle(p: TherapyProfile): string {
|
|
4
|
+
const name = fullName(p);
|
|
5
|
+
const socials = Object.entries(p.social ?? {}).filter(([, v]) => v);
|
|
6
|
+
|
|
7
|
+
const address = p.city
|
|
8
|
+
? `<address itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
|
|
9
|
+
<strong>Miasto: </strong>
|
|
10
|
+
<span itemprop="addressLocality">${esc(p.city)}</span>,
|
|
11
|
+
<span itemprop="addressCountry">PL</span>
|
|
12
|
+
</address>`
|
|
13
|
+
: '';
|
|
14
|
+
|
|
15
|
+
const bio = p.bio
|
|
16
|
+
? `<section>
|
|
17
|
+
<h2>O mnie</h2>
|
|
18
|
+
<p itemprop="description">${esc(p.bio)}</p>
|
|
19
|
+
</section>`
|
|
20
|
+
: '';
|
|
21
|
+
|
|
22
|
+
const specialties = p.specialties.length > 0
|
|
23
|
+
? `<section>
|
|
24
|
+
<h2>Specjalizacje</h2>
|
|
25
|
+
<ul>
|
|
26
|
+
${p.specialties.map(s => `<li itemprop="knowsAbout">${esc(s)}</li>`).join('\n')}
|
|
27
|
+
</ul>
|
|
28
|
+
</section>`
|
|
29
|
+
: '';
|
|
30
|
+
|
|
31
|
+
const languages = p.languages.length > 0
|
|
32
|
+
? `<section>
|
|
33
|
+
<h2>Języki</h2>
|
|
34
|
+
<ul>
|
|
35
|
+
${p.languages.map(l => `<li itemprop="knowsLanguage">${esc(l)}</li>`).join('\n')}
|
|
36
|
+
</ul>
|
|
37
|
+
</section>`
|
|
38
|
+
: '';
|
|
39
|
+
|
|
40
|
+
const socialUrlPrefix: Record<string, string> = {
|
|
41
|
+
linkedin: 'https://linkedin.com/in/',
|
|
42
|
+
youtube: 'https://youtube.com/@',
|
|
43
|
+
tiktok: 'https://tiktok.com/@',
|
|
44
|
+
facebook: 'https://facebook.com/',
|
|
45
|
+
twitter: 'https://x.com/',
|
|
46
|
+
instagram: 'https://instagram.com/',
|
|
47
|
+
};
|
|
48
|
+
const contactItems = socials.map(([n, handle]) => {
|
|
49
|
+
const prefix = socialUrlPrefix[n] ?? `https://${esc(n)}.com/`;
|
|
50
|
+
return `<dt>${esc(n)}</dt><dd><a href="${prefix}${esc(handle as string)}" rel="noopener">@${esc(handle as string)}</a></dd>`;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const contact = contactItems.length > 0
|
|
54
|
+
? `<section>
|
|
55
|
+
<h2>Kontakt</h2>
|
|
56
|
+
<dl>
|
|
57
|
+
${contactItems.join('\n')}
|
|
58
|
+
</dl>
|
|
59
|
+
</section>`
|
|
60
|
+
: '';
|
|
61
|
+
|
|
62
|
+
const business = p.business?.name
|
|
63
|
+
? `<section>
|
|
64
|
+
<h2>Dane firmy</h2>
|
|
65
|
+
<dl>
|
|
66
|
+
<dt>Nazwa</dt><dd>${esc(p.business.name)}</dd>
|
|
67
|
+
${p.business.taxId ? `<dt>NIP</dt><dd>${esc(p.business.taxId)}</dd>` : ''}
|
|
68
|
+
${(p.business.classification?.length ?? 0) > 0 ? `<dt>PKD</dt><dd>${p.business.classification!.map(c => esc(c)).join(', ')}</dd>` : ''}
|
|
69
|
+
</dl>
|
|
70
|
+
</section>`
|
|
71
|
+
: '';
|
|
72
|
+
|
|
73
|
+
return `<article itemscope itemtype="https://schema.org/Person">
|
|
74
|
+
<header>
|
|
75
|
+
<h1 itemprop="name">${esc(name)}</h1>
|
|
76
|
+
<p itemprop="jobTitle">${esc(p.jobTitle)}</p>
|
|
77
|
+
</header>
|
|
78
|
+
${address}
|
|
79
|
+
${bio}
|
|
80
|
+
${specialties}
|
|
81
|
+
${languages}
|
|
82
|
+
${contact}
|
|
83
|
+
${business}
|
|
84
|
+
</article>`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { esc, fullName, type TherapyProfile } from './helpers.ts';
|
|
2
|
+
|
|
3
|
+
export function profileCard(p: TherapyProfile, href: string): string {
|
|
4
|
+
const name = fullName(p);
|
|
5
|
+
const initials = `${(p.firstName?.[0] ?? '').toUpperCase()}${(p.lastName?.[0] ?? '').toUpperCase()}`;
|
|
6
|
+
|
|
7
|
+
const city = p.city ? `<span itemprop="address" itemscope itemtype="https://schema.org/PostalAddress"><span itemprop="addressLocality">${esc(p.city)}</span></span>` : '';
|
|
8
|
+
|
|
9
|
+
const bio = p.bio ? `<p itemprop="description">${esc(p.bio)}</p>` : '';
|
|
10
|
+
|
|
11
|
+
const specs = p.specialties.length > 0
|
|
12
|
+
? `<ul>${p.specialties.slice(0, 3).map(s => `<li>${esc(s)}</li>`).join('')}</ul>`
|
|
13
|
+
: '';
|
|
14
|
+
|
|
15
|
+
return `<article itemscope itemtype="https://schema.org/Person">
|
|
16
|
+
<div aria-hidden="true">${initials}</div>
|
|
17
|
+
<header>
|
|
18
|
+
<h2><a href="${esc(href)}" itemprop="url"><span itemprop="name">${esc(name)}</span></a></h2>
|
|
19
|
+
<p><span itemprop="jobTitle">${esc(p.jobTitle)}</span></p>
|
|
20
|
+
${city ? `<p>${city}</p>` : ''}
|
|
21
|
+
</header>
|
|
22
|
+
${bio}
|
|
23
|
+
${specs}
|
|
24
|
+
<a href="${esc(href)}">Zobacz profil →</a>
|
|
25
|
+
</article>`;
|
|
26
|
+
}
|