@jjlmoya/utils-diy 1.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.
Files changed (135) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +110 -0
  3. package/src/category/i18n/es.ts +110 -0
  4. package/src/category/i18n/fr.ts +101 -0
  5. package/src/category/index.ts +25 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +57 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/balusterCalculator/bibliography.astro +14 -0
  25. package/src/tool/balusterCalculator/component.astro +605 -0
  26. package/src/tool/balusterCalculator/i18n/en.ts +264 -0
  27. package/src/tool/balusterCalculator/i18n/es.ts +264 -0
  28. package/src/tool/balusterCalculator/i18n/fr.ts +264 -0
  29. package/src/tool/balusterCalculator/index.ts +33 -0
  30. package/src/tool/balusterCalculator/seo.astro +15 -0
  31. package/src/tool/balusterCalculator/ui.ts +15 -0
  32. package/src/tool/clayCalculator/bibliography.astro +14 -0
  33. package/src/tool/clayCalculator/component.astro +731 -0
  34. package/src/tool/clayCalculator/i18n/en.ts +183 -0
  35. package/src/tool/clayCalculator/i18n/es.ts +183 -0
  36. package/src/tool/clayCalculator/i18n/fr.ts +183 -0
  37. package/src/tool/clayCalculator/index.ts +33 -0
  38. package/src/tool/clayCalculator/seo.astro +15 -0
  39. package/src/tool/clayCalculator/ui.ts +19 -0
  40. package/src/tool/concreteCalculator/bibliography.astro +13 -0
  41. package/src/tool/concreteCalculator/component.astro +1089 -0
  42. package/src/tool/concreteCalculator/i18n/en.ts +201 -0
  43. package/src/tool/concreteCalculator/i18n/es.ts +201 -0
  44. package/src/tool/concreteCalculator/i18n/fr.ts +201 -0
  45. package/src/tool/concreteCalculator/index.ts +28 -0
  46. package/src/tool/concreteCalculator/seo.astro +14 -0
  47. package/src/tool/concreteCalculator/ui.ts +35 -0
  48. package/src/tool/cutOptimizer/bibliography.astro +13 -0
  49. package/src/tool/cutOptimizer/component.astro +825 -0
  50. package/src/tool/cutOptimizer/i18n/en.ts +178 -0
  51. package/src/tool/cutOptimizer/i18n/es.ts +178 -0
  52. package/src/tool/cutOptimizer/i18n/fr.ts +178 -0
  53. package/src/tool/cutOptimizer/index.ts +28 -0
  54. package/src/tool/cutOptimizer/optimizer.ts +199 -0
  55. package/src/tool/cutOptimizer/seo.astro +14 -0
  56. package/src/tool/cutOptimizer/ui.ts +25 -0
  57. package/src/tool/drillCalculator/bibliography.astro +13 -0
  58. package/src/tool/drillCalculator/component.astro +1210 -0
  59. package/src/tool/drillCalculator/engine.ts +63 -0
  60. package/src/tool/drillCalculator/i18n/en.ts +137 -0
  61. package/src/tool/drillCalculator/i18n/es.ts +137 -0
  62. package/src/tool/drillCalculator/i18n/fr.ts +137 -0
  63. package/src/tool/drillCalculator/index.ts +28 -0
  64. package/src/tool/drillCalculator/seo.astro +14 -0
  65. package/src/tool/drillCalculator/ui.ts +41 -0
  66. package/src/tool/drillSharpener/bibliography.astro +13 -0
  67. package/src/tool/drillSharpener/component.astro +712 -0
  68. package/src/tool/drillSharpener/engine.ts +42 -0
  69. package/src/tool/drillSharpener/i18n/en.ts +121 -0
  70. package/src/tool/drillSharpener/i18n/es.ts +121 -0
  71. package/src/tool/drillSharpener/i18n/fr.ts +121 -0
  72. package/src/tool/drillSharpener/index.ts +31 -0
  73. package/src/tool/drillSharpener/seo.astro +14 -0
  74. package/src/tool/drillSharpener/ui.ts +53 -0
  75. package/src/tool/epoxyCalculator/bibliography.astro +14 -0
  76. package/src/tool/epoxyCalculator/component.astro +888 -0
  77. package/src/tool/epoxyCalculator/i18n/en.ts +195 -0
  78. package/src/tool/epoxyCalculator/i18n/es.ts +195 -0
  79. package/src/tool/epoxyCalculator/i18n/fr.ts +195 -0
  80. package/src/tool/epoxyCalculator/index.ts +33 -0
  81. package/src/tool/epoxyCalculator/seo.astro +15 -0
  82. package/src/tool/epoxyCalculator/ui.ts +22 -0
  83. package/src/tool/furnitureFit/bibliography.astro +13 -0
  84. package/src/tool/furnitureFit/component.astro +552 -0
  85. package/src/tool/furnitureFit/engine.ts +51 -0
  86. package/src/tool/furnitureFit/i18n/en.ts +152 -0
  87. package/src/tool/furnitureFit/i18n/es.ts +152 -0
  88. package/src/tool/furnitureFit/i18n/fr.ts +152 -0
  89. package/src/tool/furnitureFit/index.ts +28 -0
  90. package/src/tool/furnitureFit/seo.astro +14 -0
  91. package/src/tool/furnitureFit/ui.ts +21 -0
  92. package/src/tool/mortarCalculator/bibliography.astro +13 -0
  93. package/src/tool/mortarCalculator/component.astro +1139 -0
  94. package/src/tool/mortarCalculator/i18n/en.ts +219 -0
  95. package/src/tool/mortarCalculator/i18n/es.ts +205 -0
  96. package/src/tool/mortarCalculator/i18n/fr.ts +220 -0
  97. package/src/tool/mortarCalculator/index.ts +24 -0
  98. package/src/tool/mortarCalculator/seo.astro +14 -0
  99. package/src/tool/mortarCalculator/ui.ts +37 -0
  100. package/src/tool/passepartoutCalculator/bibliography.astro +13 -0
  101. package/src/tool/passepartoutCalculator/component.astro +741 -0
  102. package/src/tool/passepartoutCalculator/i18n/en.ts +178 -0
  103. package/src/tool/passepartoutCalculator/i18n/es.ts +178 -0
  104. package/src/tool/passepartoutCalculator/i18n/fr.ts +178 -0
  105. package/src/tool/passepartoutCalculator/index.ts +28 -0
  106. package/src/tool/passepartoutCalculator/seo.astro +14 -0
  107. package/src/tool/passepartoutCalculator/ui.ts +17 -0
  108. package/src/tool/stairCalculator/bibliography.astro +52 -0
  109. package/src/tool/stairCalculator/component.astro +766 -0
  110. package/src/tool/stairCalculator/engine.ts +128 -0
  111. package/src/tool/stairCalculator/i18n/en.ts +149 -0
  112. package/src/tool/stairCalculator/i18n/es.ts +149 -0
  113. package/src/tool/stairCalculator/i18n/fr.ts +149 -0
  114. package/src/tool/stairCalculator/index.ts +31 -0
  115. package/src/tool/stairCalculator/seo.astro +211 -0
  116. package/src/tool/stairCalculator/ui.ts +109 -0
  117. package/src/tool/thermalExpansionCalculator/bibliography.astro +13 -0
  118. package/src/tool/thermalExpansionCalculator/component.astro +771 -0
  119. package/src/tool/thermalExpansionCalculator/engine.ts +23 -0
  120. package/src/tool/thermalExpansionCalculator/i18n/en.ts +174 -0
  121. package/src/tool/thermalExpansionCalculator/i18n/es.ts +174 -0
  122. package/src/tool/thermalExpansionCalculator/i18n/fr.ts +174 -0
  123. package/src/tool/thermalExpansionCalculator/index.ts +28 -0
  124. package/src/tool/thermalExpansionCalculator/seo.astro +14 -0
  125. package/src/tool/thermalExpansionCalculator/ui.ts +41 -0
  126. package/src/tool/voltageDropCalculator/bibliography.astro +13 -0
  127. package/src/tool/voltageDropCalculator/component.astro +1022 -0
  128. package/src/tool/voltageDropCalculator/i18n/en.ts +155 -0
  129. package/src/tool/voltageDropCalculator/i18n/es.ts +155 -0
  130. package/src/tool/voltageDropCalculator/i18n/fr.ts +148 -0
  131. package/src/tool/voltageDropCalculator/index.ts +28 -0
  132. package/src/tool/voltageDropCalculator/seo.astro +14 -0
  133. package/src/tool/voltageDropCalculator/ui.ts +22 -0
  134. package/src/tools.ts +16 -0
  135. package/src/types.ts +72 -0
@@ -0,0 +1,143 @@
1
+ ---
2
+ import type { KnownLocale } from '../types';
3
+
4
+ interface Props {
5
+ currentLocale: KnownLocale;
6
+ localeUrls?: Partial<Record<KnownLocale, string>>;
7
+ }
8
+
9
+ const { currentLocale, localeUrls = {} } = Astro.props;
10
+
11
+ const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLocale) as [
12
+ KnownLocale,
13
+ string,
14
+ ][];
15
+ ---
16
+
17
+ <nav class="preview-toolbar">
18
+ {
19
+ otherLocales.length > 0 && (
20
+ <div class="locale-group">
21
+ <span class="locale-current">{currentLocale.toUpperCase()}</span>
22
+ {otherLocales.map(([locale, url]) => (
23
+ <a href={url} class="btn-locale">
24
+ {locale.toUpperCase()}
25
+ </a>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
30
+
31
+ <button id="theme-toggle" class="btn-theme" aria-label="Toggle theme" title="Toggle theme">
32
+ <span class="theme-icon"></span>
33
+ </button>
34
+ </nav>
35
+
36
+ <script>
37
+ function initToolbar() {
38
+ const btn = document.getElementById('theme-toggle');
39
+ if (!btn) return;
40
+
41
+ const applyTheme = (theme: string) => {
42
+ document.documentElement.classList.remove('theme-light', 'theme-dark');
43
+ document.documentElement.classList.add(theme);
44
+ localStorage.setItem('theme', theme);
45
+ };
46
+
47
+ btn.addEventListener('click', () => {
48
+ const next = document.documentElement.classList.contains('theme-dark')
49
+ ? 'theme-light'
50
+ : 'theme-dark';
51
+ applyTheme(next);
52
+ });
53
+ }
54
+
55
+ initToolbar();
56
+ document.addEventListener('astro:after-swap', initToolbar);
57
+ </script>
58
+
59
+ <style>
60
+ .preview-toolbar {
61
+ position: fixed;
62
+ top: 1rem;
63
+ right: 1.5rem;
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 0.5rem;
67
+ z-index: 1000;
68
+ background: var(--bg-surface);
69
+ padding: 0.4rem;
70
+ border-radius: 1rem;
71
+ border: 1px solid var(--border-color);
72
+ backdrop-filter: blur(12px);
73
+ }
74
+
75
+ .locale-group {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.25rem;
79
+ padding-right: 0.5rem;
80
+ border-right: 1px solid var(--border-color);
81
+ }
82
+
83
+ .locale-current {
84
+ font-size: 0.7rem;
85
+ font-weight: 900;
86
+ color: var(--text-muted);
87
+ padding: 0.4rem 0.6rem;
88
+ letter-spacing: 0.08em;
89
+ }
90
+
91
+ .btn-locale {
92
+ background: transparent;
93
+ border: 1px solid var(--border-color);
94
+ color: var(--text-main);
95
+ cursor: pointer;
96
+ padding: 0.4rem 0.7rem;
97
+ border-radius: 0.5rem;
98
+ font-weight: 700;
99
+ font-size: 0.7rem;
100
+ text-decoration: none;
101
+ letter-spacing: 0.08em;
102
+ transition: all 0.2s ease;
103
+ }
104
+
105
+ .btn-locale:hover {
106
+ border-color: var(--accent);
107
+ color: var(--accent);
108
+ }
109
+
110
+ .btn-theme {
111
+ background: transparent;
112
+ border: none;
113
+ cursor: pointer;
114
+ padding: 0.5rem;
115
+ border-radius: 0.6rem;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ transition: background 0.2s ease;
120
+ }
121
+
122
+ .btn-theme:hover {
123
+ background: rgba(255, 255, 255, 0.08);
124
+ }
125
+
126
+ .theme-icon {
127
+ display: block;
128
+ width: 16px;
129
+ height: 16px;
130
+ border-radius: 50%;
131
+ }
132
+
133
+ :global(.theme-light) .theme-icon {
134
+ background: #f59e0b;
135
+ box-shadow: 0 0 8px #fbbf24;
136
+ }
137
+
138
+ :global(.theme-dark) .theme-icon {
139
+ background: #94a3b8;
140
+ box-shadow: inset -4px -2px 0 #1e293b;
141
+ }
142
+ </style>
143
+
package/src/data.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { diyCategory } from './category';
2
+
3
+ export type {
4
+ KnownLocale,
5
+ ToolLocaleContent,
6
+ CategoryLocaleContent,
7
+ LocaleMap,
8
+ DiyToolEntry,
9
+ DiyCategoryEntry,
10
+ } from './types';
11
+
package/src/env.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module '*.astro' {
2
+ const Component: (_props: Record<string, unknown>) => unknown;
3
+ export default Component;
4
+ }
5
+
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ export { diyCategory } from './category';
2
+ export { default as diyCategorySEO } from './category/seo.astro';
3
+
4
+ export type {
5
+ KnownLocale,
6
+ FAQItem,
7
+ BibliographyEntry,
8
+ HowToStep,
9
+ ToolLocaleContent,
10
+ CategoryLocaleContent,
11
+ LocaleLoader,
12
+ LocaleMap,
13
+ DiyToolEntry,
14
+ DiyCategoryEntry,
15
+ ToolDefinition,
16
+ } from './types';
17
+
18
+ export { ALL_TOOLS } from './tools';
19
+
20
+ export { ClayCalculatorComponent, ClayCalculatorSEO, ClayCalculatorBibliography } from './tool/clayCalculator';
21
+ export { CLAY_CALCULATOR_TOOL } from './tool/clayCalculator/index';
22
+
23
+ export { EpoxyCalculatorComponent, EpoxyCalculatorSEO, EpoxyCalculatorBibliography } from './tool/epoxyCalculator';
24
+ export { EPOXY_CALCULATOR_TOOL } from './tool/epoxyCalculator/index';
25
+
26
+ export { BalusterCalculatorComponent, BalusterCalculatorSEO, BalusterCalculatorBibliography } from './tool/balusterCalculator';
27
+ export { BALUSTER_CALCULATOR_TOOL } from './tool/balusterCalculator/index';
28
+
29
+ export { MortarCalculatorComponent, MortarCalculatorSEO, MortarCalculatorBibliography } from './tool/mortarCalculator';
30
+ export { MORTAR_CALCULATOR_TOOL } from './tool/mortarCalculator/index';
31
+
32
+ export { PassepartoutCalculatorComponent, PassepartoutCalculatorSEO, PassepartoutCalculatorBibliography } from './tool/passepartoutCalculator';
33
+ export { PASSEPARTOUT_CALCULATOR_TOOL } from './tool/passepartoutCalculator/index';
34
+
35
+ export { ConcreteCalculatorComponent, ConcreteCalculatorSEO, ConcreteCalculatorBibliography } from './tool/concreteCalculator';
36
+ export { CONCRETE_CALCULATOR_TOOL } from './tool/concreteCalculator/index';
37
+
38
+ export { CutOptimizerComponent, CutOptimizerSEO, CutOptimizerBibliography } from './tool/cutOptimizer';
39
+ export { CUT_OPTIMIZER_TOOL } from './tool/cutOptimizer/index';
40
+
41
+ export { VoltageDropCalculatorComponent, VoltageDropCalculatorSEO, VoltageDropCalculatorBibliography } from './tool/voltageDropCalculator';
42
+ export { VOLTAGE_DROP_CALCULATOR_TOOL } from './tool/voltageDropCalculator/index';
43
+
44
+ export { FurnitureFitComponent, FurnitureFitSEO, FurnitureFitBibliography } from './tool/furnitureFit';
45
+ export { FURNITURE_FIT_TOOL } from './tool/furnitureFit/index';
46
+
47
+ export { ThermalExpansionCalculatorComponent, ThermalExpansionCalculatorSEO, ThermalExpansionCalculatorBibliography } from './tool/thermalExpansionCalculator';
48
+ export { THERMAL_EXPANSION_CALCULATOR_TOOL } from './tool/thermalExpansionCalculator/index';
49
+
50
+ export { DrillCalculatorComponent, DrillCalculatorSEO, DrillCalculatorBibliography } from './tool/drillCalculator';
51
+ export { DRILL_CALCULATOR_TOOL } from './tool/drillCalculator/index';
52
+
53
+ export { StairCalculatorComponent, StairCalculatorSEO, StairCalculatorBibliography } from './tool/stairCalculator';
54
+ export { STAIR_CALCULATOR_TOOL } from './tool/stairCalculator/index';
55
+
56
+ export { DrillSharpenerComponent, DrillSharpenerSEO, DrillSharpenerBibliography } from './tool/drillSharpener';
57
+ export { DRILL_SHARPENER_TOOL } from './tool/drillSharpener/index';
@@ -0,0 +1,117 @@
1
+ ---
2
+ import "@jjlmoya/utils-shared/theme.css";
3
+ import PreviewToolbar from "../components/PreviewToolbar.astro";
4
+ import type { KnownLocale } from "../types";
5
+
6
+ interface Props {
7
+ title: string;
8
+ currentLocale?: KnownLocale;
9
+ localeUrls?: Partial<Record<KnownLocale, string>>;
10
+ hasSidebar?: boolean;
11
+ }
12
+
13
+ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Astro.props;
14
+ ---
15
+
16
+ <!doctype html>
17
+ <html lang={currentLocale}>
18
+ <head>
19
+ <meta charset="UTF-8" />
20
+ <meta name="viewport" content="width=device-width" />
21
+ <title>{title} · preview</title>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
24
+ <link
25
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
26
+ rel="stylesheet"
27
+ />
28
+
29
+ <script is:inline>
30
+ (function () {
31
+ const saved = localStorage.getItem("theme") || "theme-dark";
32
+ document.documentElement.classList.add(saved);
33
+ })();
34
+ </script>
35
+ <slot name="head" />
36
+ </head>
37
+ <body>
38
+ <PreviewToolbar currentLocale={currentLocale} localeUrls={localeUrls} />
39
+ <div class:list={["page-wrapper", { "with-sidebar": hasSidebar }]}>
40
+ {
41
+ hasSidebar && (
42
+ <aside class="sidebar-area">
43
+ <slot name="sidebar" />
44
+ </aside>
45
+ )
46
+ }
47
+ <main>
48
+ <slot />
49
+ </main>
50
+ </div>
51
+ </body>
52
+ </html>
53
+
54
+ <style is:global>
55
+ :root {
56
+ --accent: #f43f5e;
57
+ --primary-base: #9f1239;
58
+ --cyan: #06b6d4;
59
+ }
60
+
61
+ .theme-dark,
62
+ .theme-light {
63
+ --text-main: var(--text-base);
64
+ --border-color: var(--border-base);
65
+ }
66
+
67
+ *,
68
+ *::before,
69
+ *::after {
70
+ box-sizing: border-box;
71
+ }
72
+
73
+ body {
74
+ background-color: var(--bg-page);
75
+ color: var(--text-base);
76
+ margin: 0;
77
+ min-height: 100vh;
78
+ transition:
79
+ background-color 0.3s ease,
80
+ color 0.3s ease;
81
+ }
82
+
83
+ main {
84
+ padding: 0 2rem;
85
+ }
86
+
87
+ .page-wrapper {
88
+ display: flex;
89
+ flex-direction: column;
90
+ }
91
+
92
+ .page-wrapper.with-sidebar {
93
+ display: grid;
94
+ grid-template-columns: 240px 1fr;
95
+ min-height: 100vh;
96
+ }
97
+
98
+ .sidebar-area {
99
+ position: sticky;
100
+ top: 0;
101
+ height: 100vh;
102
+ overflow-y: auto;
103
+ border-right: 1px solid var(--border-color);
104
+ background: var(--bg-page);
105
+ }
106
+
107
+ @media (max-width: 768px) {
108
+ .page-wrapper.with-sidebar {
109
+ grid-template-columns: 1fr;
110
+ }
111
+
112
+ .sidebar-area {
113
+ display: none;
114
+ }
115
+ }
116
+ </style>
117
+
@@ -0,0 +1,146 @@
1
+ ---
2
+ import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
+ import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
+ import { ALL_TOOLS } from "../../index";
5
+ import {
6
+ UtilityHeader,
7
+ FAQSection,
8
+ Bibliography,
9
+ SEORenderer,
10
+ } from "@jjlmoya/utils-shared";
11
+ import type { KnownLocale, ToolLocaleContent } from "../../types";
12
+ import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
+
14
+ export async function getStaticPaths() {
15
+ const paths = [];
16
+
17
+ for (const { entry, Component } of ALL_TOOLS) {
18
+ const localeEntries = Object.entries(entry.i18n) as [
19
+ KnownLocale,
20
+ () => Promise<ToolLocaleContent>,
21
+ ][];
22
+ const localeContents = await Promise.all(
23
+ localeEntries.map(async ([locale, loader]) => ({
24
+ locale,
25
+ content: await loader(),
26
+ })),
27
+ );
28
+
29
+ const localeUrls = Object.fromEntries(
30
+ localeContents.map(({ locale, content }) => [
31
+ locale,
32
+ `/${locale}/${content.slug}`,
33
+ ]),
34
+ ) as Partial<Record<KnownLocale, string>>;
35
+
36
+ for (const { locale, content } of localeContents) {
37
+ const allToolsNav = await Promise.all(
38
+ ALL_TOOLS.map(async ({ entry: navEntry }) => ({
39
+ id: navEntry.id,
40
+ title: (await navEntry.i18n[locale]!()).title,
41
+ href: `/${locale}/${(await navEntry.i18n[locale]!()).slug}`,
42
+ isActive: navEntry.id === entry.id,
43
+ })),
44
+ );
45
+ paths.push({
46
+ params: { locale, slug: content.slug },
47
+ props: { Component, locale, content, localeUrls, allToolsNav },
48
+ });
49
+ }
50
+ }
51
+
52
+ return paths;
53
+ }
54
+
55
+ type ToolComponent = (props: { ui: Record<string, string> }) => unknown;
56
+
57
+ interface NavItem {
58
+ id: string;
59
+ title: string;
60
+ href: string;
61
+ isActive?: boolean;
62
+ }
63
+
64
+ interface Props {
65
+ Component: ToolComponent;
66
+ locale: KnownLocale;
67
+ content: ToolLocaleContent;
68
+ localeUrls: Partial<Record<KnownLocale, string>>;
69
+ allToolsNav: NavItem[];
70
+ }
71
+
72
+ const { Component, locale, content, localeUrls, allToolsNav } = Astro.props;
73
+
74
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo };
75
+
76
+ const words = content.title.split(" ");
77
+ const titleHighlight = words[0] || "";
78
+ const titleBase = words.slice(1).join(" ") || "";
79
+ ---
80
+
81
+ <PreviewLayout
82
+ title={content.title}
83
+ currentLocale={locale}
84
+ localeUrls={localeUrls}
85
+ hasSidebar={true}
86
+ >
87
+ <PreviewNavSidebar
88
+ slot="sidebar"
89
+ categoryTitle="Tools"
90
+ tools={allToolsNav}
91
+ />
92
+ <Fragment slot="head">
93
+ {
94
+ content.schemas.map((schema: unknown) => (
95
+ <script
96
+ is:inline
97
+ type="application/ld+json"
98
+ set:html={JSON.stringify(schema)}
99
+ />
100
+ ))
101
+ }
102
+ </Fragment>
103
+
104
+ <div class="tool-page">
105
+ <UtilityHeader
106
+ titleHighlight={titleHighlight}
107
+ titleBase={titleBase}
108
+ description={content.description}
109
+ />
110
+
111
+ <section class="section-tool">
112
+ <Component ui={content.ui} />
113
+ </section>
114
+
115
+ <section class="section-seo">
116
+ <SEORenderer content={seoContent} />
117
+ </section>
118
+
119
+ <section class="section-faq">
120
+ <FAQSection items={content.faq} title={content.ui.faqTitle} inLanguage={locale} />
121
+ </section>
122
+
123
+ <section class="section-bibliography">
124
+ <Bibliography links={content.bibliography} title={content.ui.bibliographyTitle} />
125
+ </section>
126
+ </div>
127
+ </PreviewLayout>
128
+
129
+ <style>
130
+ .tool-page {
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: 2rem;
134
+ }
135
+ .section-tool {
136
+ max-width: 1200px;
137
+ margin: 0 auto;
138
+ width: 100%;
139
+ }
140
+ .section-seo,
141
+ .section-faq,
142
+ .section-bibliography {
143
+ padding-top: 2rem;
144
+ border-top: 1px solid var(--border-color);
145
+ }
146
+ </style>