@jjlmoya/utils-alcohol 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.
- package/package.json +60 -0
- package/src/category/i18n/en.ts +19 -0
- package/src/category/i18n/es.ts +28 -0
- package/src/category/i18n/fr.ts +19 -0
- package/src/category/index.ts +12 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +19 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +155 -0
- package/src/pages/[locale].astro +271 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/content_mandatory.test.ts +32 -0
- package/src/tests/faq_count.test.ts +17 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/seo_length.test.ts +39 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/alcoholClearance/component.astro +219 -0
- package/src/tool/alcoholClearance/component.css +369 -0
- package/src/tool/alcoholClearance/i18n/en.ts +172 -0
- package/src/tool/alcoholClearance/i18n/es.ts +181 -0
- package/src/tool/alcoholClearance/i18n/fr.ts +163 -0
- package/src/tool/alcoholClearance/index.ts +50 -0
- package/src/tool/alcoholClearance/logic.ts +59 -0
- package/src/tool/beerCooler/component.astro +236 -0
- package/src/tool/beerCooler/component.css +381 -0
- package/src/tool/beerCooler/i18n/en.ts +168 -0
- package/src/tool/beerCooler/i18n/es.ts +181 -0
- package/src/tool/beerCooler/i18n/fr.ts +168 -0
- package/src/tool/beerCooler/index.ts +49 -0
- package/src/tool/beerCooler/logic.ts +34 -0
- package/src/tool/carbonationCalculator/component.astro +225 -0
- package/src/tool/carbonationCalculator/component.css +483 -0
- package/src/tool/carbonationCalculator/i18n/en.ts +175 -0
- package/src/tool/carbonationCalculator/i18n/es.ts +179 -0
- package/src/tool/carbonationCalculator/i18n/fr.ts +175 -0
- package/src/tool/carbonationCalculator/index.ts +48 -0
- package/src/tool/carbonationCalculator/logic.ts +40 -0
- package/src/tool/cocktailBalancer/bibliography.astro +14 -0
- package/src/tool/cocktailBalancer/component.astro +396 -0
- package/src/tool/cocktailBalancer/component.css +1218 -0
- package/src/tool/cocktailBalancer/data/IngredientRepository.ts +83 -0
- package/src/tool/cocktailBalancer/data/Presets.ts +122 -0
- package/src/tool/cocktailBalancer/domain/Ingredient.ts +29 -0
- package/src/tool/cocktailBalancer/i18n/en.ts +193 -0
- package/src/tool/cocktailBalancer/i18n/es.ts +193 -0
- package/src/tool/cocktailBalancer/i18n/fr.ts +193 -0
- package/src/tool/cocktailBalancer/index.ts +68 -0
- package/src/tool/cocktailBalancer/logic.ts +118 -0
- package/src/tool/cocktailBalancer/seo.astro +53 -0
- package/src/tool/partyKeg/component.astro +269 -0
- package/src/tool/partyKeg/component.css +660 -0
- package/src/tool/partyKeg/i18n/en.ts +162 -0
- package/src/tool/partyKeg/i18n/es.ts +166 -0
- package/src/tool/partyKeg/i18n/fr.ts +162 -0
- package/src/tool/partyKeg/index.ts +46 -0
- package/src/tool/partyKeg/logic.ts +36 -0
- package/src/tools.ts +14 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,155 @@
|
|
|
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) => (
|
|
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
|
|
121
|
+
title={content.faqTitle}
|
|
122
|
+
items={content.faq}
|
|
123
|
+
inLanguage={locale}
|
|
124
|
+
/>
|
|
125
|
+
</section>
|
|
126
|
+
|
|
127
|
+
<section class="section-bibliography">
|
|
128
|
+
<Bibliography
|
|
129
|
+
title={content.bibliographyTitle}
|
|
130
|
+
links={content.bibliography}
|
|
131
|
+
/>
|
|
132
|
+
</section>
|
|
133
|
+
</div>
|
|
134
|
+
</PreviewLayout>
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
.tool-page {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
gap: 2rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.section-tool {
|
|
144
|
+
max-width: 1200px;
|
|
145
|
+
margin: 0 auto;
|
|
146
|
+
width: 100%;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.section-seo,
|
|
150
|
+
.section-faq,
|
|
151
|
+
.section-bibliography {
|
|
152
|
+
padding-top: 2rem;
|
|
153
|
+
border-top: 1px solid var(--border-color);
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
---
|
|
2
|
+
import PreviewLayout from '../layouts/PreviewLayout.astro';
|
|
3
|
+
import PreviewNavSidebar from '../components/PreviewNavSidebar.astro';
|
|
4
|
+
import { alcoholCategory, ALL_TOOLS } from '../index';
|
|
5
|
+
import { Icon } from 'astro-icon/components';
|
|
6
|
+
import type { KnownLocale, ToolLocaleContent } from '../types';
|
|
7
|
+
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
const locales = ['en', 'es', 'fr'] as KnownLocale[];
|
|
10
|
+
return locales.map(locale => ({ params: { locale } }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { locale: currentLocale } = Astro.params as { locale: KnownLocale };
|
|
14
|
+
|
|
15
|
+
const categoryContent = await alcoholCategory.i18n[currentLocale]!();
|
|
16
|
+
|
|
17
|
+
const tools = ALL_TOOLS || [];
|
|
18
|
+
|
|
19
|
+
const toolsWithContent = tools.length > 0
|
|
20
|
+
? await Promise.all(
|
|
21
|
+
tools.map(async ({ entry, Component }) => {
|
|
22
|
+
const languages = Object.keys(entry.i18n);
|
|
23
|
+
const localeEntries = await Promise.all(
|
|
24
|
+
languages.map(async (l) => {
|
|
25
|
+
const content = await entry.i18n[l as KnownLocale]!();
|
|
26
|
+
return [l, content];
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const localeContents = Object.fromEntries(localeEntries) as Record<string, ToolLocaleContent<Record<string, string>>>;
|
|
31
|
+
|
|
32
|
+
const currentLocaleContent = localeContents[currentLocale] || localeContents['en'] || localeContents['es'];
|
|
33
|
+
const availableLocales: Record<string, string> = {};
|
|
34
|
+
|
|
35
|
+
for (const l of languages) {
|
|
36
|
+
const lCont = localeContents[l];
|
|
37
|
+
if (lCont) {
|
|
38
|
+
availableLocales[l] = `/${l}/${lCont.slug}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { entry, Component, locale: currentLocaleContent, availableLocales };
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
: [];
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<PreviewLayout
|
|
49
|
+
title={categoryContent.title}
|
|
50
|
+
currentLocale={currentLocale}
|
|
51
|
+
hasSidebar={true}
|
|
52
|
+
>
|
|
53
|
+
<PreviewNavSidebar
|
|
54
|
+
slot="sidebar"
|
|
55
|
+
categoryTitle={categoryContent.title}
|
|
56
|
+
tools={toolsWithContent.map(({ entry, locale, availableLocales }) => {
|
|
57
|
+
const href = availableLocales[currentLocale] || (locale ? `/${currentLocale}/${locale.slug}` : '#');
|
|
58
|
+
return {
|
|
59
|
+
id: entry.id,
|
|
60
|
+
title: locale?.title || entry.id,
|
|
61
|
+
href: href,
|
|
62
|
+
};
|
|
63
|
+
})}
|
|
64
|
+
/>
|
|
65
|
+
<div class="dashboard">
|
|
66
|
+
<header class="preview-header">
|
|
67
|
+
<span class="badge">preview · @jjlmoya/utils-template</span>
|
|
68
|
+
<h1>{categoryContent.title}</h1>
|
|
69
|
+
<p>{categoryContent.description}</p>
|
|
70
|
+
</header>
|
|
71
|
+
|
|
72
|
+
<div class="tool-list">
|
|
73
|
+
{toolsWithContent?.map(({ entry, locale, availableLocales }) => (
|
|
74
|
+
<article class="tool-card">
|
|
75
|
+
<a href={availableLocales?.[currentLocale] || (locale ? `/${currentLocale}/${locale.slug}` : '#')} class="tool-card-link">
|
|
76
|
+
<div class="tool-icons">
|
|
77
|
+
<div class="icon-wrapper bg">
|
|
78
|
+
<Icon name={entry.icons.bg} />
|
|
79
|
+
</div>
|
|
80
|
+
<div class="icon-wrapper fg">
|
|
81
|
+
<Icon name={entry.icons.fg} />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="tool-card-content">
|
|
85
|
+
<h2 class="tool-title">{locale?.title}</h2>
|
|
86
|
+
<p class="tool-description">{locale?.description}</p>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="tool-card-meta">
|
|
89
|
+
<span class="tool-id">{entry.id}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</a>
|
|
92
|
+
|
|
93
|
+
{availableLocales && Object.keys(availableLocales).length > 1 && (
|
|
94
|
+
<div class="tool-locales">
|
|
95
|
+
{Object.entries(availableLocales).map(([l, url]) => (
|
|
96
|
+
<a href={url} class="locale-badge" title={`Ver en ${l.toUpperCase()}`} class:list={{ active: l === currentLocale }}>
|
|
97
|
+
{l.toUpperCase()}
|
|
98
|
+
</a>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</article>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</PreviewLayout>
|
|
107
|
+
|
|
108
|
+
<style>
|
|
109
|
+
.dashboard {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
gap: 5rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.preview-header {
|
|
116
|
+
text-align: center;
|
|
117
|
+
padding-bottom: 3rem;
|
|
118
|
+
border-bottom: 1px solid var(--border-color);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.badge {
|
|
122
|
+
display: inline-block;
|
|
123
|
+
padding: 0.25rem 0.75rem;
|
|
124
|
+
background: var(--accent);
|
|
125
|
+
border-radius: 99px;
|
|
126
|
+
font-size: 0.7rem;
|
|
127
|
+
font-weight: 800;
|
|
128
|
+
margin-bottom: 1.5rem;
|
|
129
|
+
color: var(--text-base);
|
|
130
|
+
letter-spacing: 0.05em;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
h1 {
|
|
134
|
+
font-size: clamp(2rem, 6vw, 3.5rem);
|
|
135
|
+
font-weight: 900;
|
|
136
|
+
margin: 0 0 1rem;
|
|
137
|
+
background: linear-gradient(to bottom, var(--text-base), var(--text-muted));
|
|
138
|
+
-webkit-background-clip: text;
|
|
139
|
+
-webkit-text-fill-color: transparent;
|
|
140
|
+
background-clip: text;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.preview-header p {
|
|
144
|
+
color: var(--text-muted);
|
|
145
|
+
font-size: 1.1rem;
|
|
146
|
+
margin: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.tool-list {
|
|
150
|
+
display: grid;
|
|
151
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
152
|
+
gap: 2rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.tool-card {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: 1rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.tool-card-link {
|
|
162
|
+
flex: 1;
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
padding: 1.5rem;
|
|
166
|
+
background: var(--bg-surface);
|
|
167
|
+
border: 1px solid var(--border-color);
|
|
168
|
+
border-radius: 0.75rem;
|
|
169
|
+
text-decoration: none;
|
|
170
|
+
transition: all 0.2s ease;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.tool-card-link:hover {
|
|
174
|
+
border-color: var(--accent);
|
|
175
|
+
background: rgba(244, 63, 94, 0.05);
|
|
176
|
+
transform: translateY(-2px);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.tool-icons {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 1rem;
|
|
183
|
+
margin-bottom: 1.25rem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.icon-wrapper {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
width: 3rem;
|
|
191
|
+
height: 3rem;
|
|
192
|
+
border-radius: 0.5rem;
|
|
193
|
+
font-size: 1.5rem;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.icon-wrapper.bg {
|
|
197
|
+
background: var(--accent);
|
|
198
|
+
color: var(--text-base);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.icon-wrapper.fg {
|
|
202
|
+
background: var(--bg-page);
|
|
203
|
+
border: 1px solid var(--border-color);
|
|
204
|
+
color: var(--accent);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.tool-card-content {
|
|
208
|
+
flex: 1;
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.tool-title {
|
|
214
|
+
font-size: 1.25rem;
|
|
215
|
+
font-weight: 700;
|
|
216
|
+
margin: 0 0 0.5rem;
|
|
217
|
+
color: var(--text-base);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.tool-description {
|
|
221
|
+
font-size: 0.9375rem;
|
|
222
|
+
color: var(--text-muted);
|
|
223
|
+
line-height: 1.5;
|
|
224
|
+
margin: 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.tool-card-meta {
|
|
228
|
+
padding-top: 1rem;
|
|
229
|
+
border-top: 1px solid var(--border-color);
|
|
230
|
+
margin-top: 1.5rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.tool-id {
|
|
234
|
+
display: inline-block;
|
|
235
|
+
font-size: 0.7rem;
|
|
236
|
+
background: var(--bg-page);
|
|
237
|
+
border: 1px solid var(--border-color);
|
|
238
|
+
padding: 0.35rem 0.75rem;
|
|
239
|
+
border-radius: 0.4rem;
|
|
240
|
+
color: var(--accent);
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.tool-locales {
|
|
245
|
+
display: flex;
|
|
246
|
+
gap: 0.5rem;
|
|
247
|
+
flex-wrap: wrap;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.locale-badge {
|
|
251
|
+
display: inline-block;
|
|
252
|
+
padding: 0.4rem 0.85rem;
|
|
253
|
+
background: var(--bg-page);
|
|
254
|
+
border: 1px solid var(--border-color);
|
|
255
|
+
border-radius: 0.4rem;
|
|
256
|
+
color: var(--text-muted);
|
|
257
|
+
text-decoration: none;
|
|
258
|
+
font-size: 0.75rem;
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
text-transform: uppercase;
|
|
261
|
+
letter-spacing: 0.05em;
|
|
262
|
+
transition: all 0.15s ease;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.locale-badge:hover,
|
|
266
|
+
.locale-badge.active {
|
|
267
|
+
color: var(--accent);
|
|
268
|
+
border-color: var(--accent);
|
|
269
|
+
background: rgba(244, 63, 94, 0.1);
|
|
270
|
+
}
|
|
271
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import type { ToolLocaleContent } from '../types';
|
|
4
|
+
|
|
5
|
+
describe('Mandatory Tool Content Validation', () => {
|
|
6
|
+
ALL_TOOLS.forEach((tool) => {
|
|
7
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
8
|
+
const locales = Object.keys(tool.entry.i18n) as (keyof typeof tool.entry.i18n)[];
|
|
9
|
+
|
|
10
|
+
locales.forEach((locale) => {
|
|
11
|
+
it(`should have non-empty FAQ and Bibliography in locale: ${locale}`, async () => {
|
|
12
|
+
const loader = tool.entry.i18n[locale];
|
|
13
|
+
if (!loader) return;
|
|
14
|
+
|
|
15
|
+
const content = await loader() as ToolLocaleContent;
|
|
16
|
+
|
|
17
|
+
expect(content.faqTitle, `FAQ title is missing for tool ${tool.entry.id} in locale ${locale}`).toBeDefined();
|
|
18
|
+
expect(content.faqTitle.length, `FAQ title should not be empty for tool ${tool.entry.id} in locale ${locale}`).toBeGreaterThan(0);
|
|
19
|
+
|
|
20
|
+
expect(content.faq, `FAQ is missing or empty for tool ${tool.entry.id} in locale ${locale}`).toBeDefined();
|
|
21
|
+
expect(content.faq.length, `FAQ should have at least one entry for tool ${tool.entry.id} in locale ${locale}`).toBeGreaterThan(0);
|
|
22
|
+
|
|
23
|
+
expect(content.bibliographyTitle, `Bibliography title is missing for tool ${tool.entry.id} in locale ${locale}`).toBeDefined();
|
|
24
|
+
expect(content.bibliographyTitle.length, `Bibliography title should not be empty for tool ${tool.entry.id} in locale ${locale}`).toBeGreaterThan(0);
|
|
25
|
+
|
|
26
|
+
expect(content.bibliography, `Bibliography is missing or empty for tool ${tool.entry.id} in locale ${locale}`).toBeDefined();
|
|
27
|
+
expect(content.bibliography.length, `Bibliography should have at least one entry for tool ${tool.entry.id} in locale ${locale}`).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const MIN_FAQS = 3;
|
|
5
|
+
|
|
6
|
+
describe('FAQ Count Validation', () => {
|
|
7
|
+
ALL_TOOLS.forEach((tool) => {
|
|
8
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
9
|
+
Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
|
|
10
|
+
it(`${locale}: should have at least ${MIN_FAQS} FAQs`, async () => {
|
|
11
|
+
const content = await loader();
|
|
12
|
+
expect(content.faq.length).toBeGreaterThanOrEqual(MIN_FAQS);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import type { SEOSection } from '../types';
|
|
4
|
+
|
|
5
|
+
const MIN_WORDS = 400;
|
|
6
|
+
|
|
7
|
+
function extractText(section: SEOSection): string {
|
|
8
|
+
const s = section as Record<string, unknown>;
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
if (typeof s['text'] === 'string') parts.push(s['text']);
|
|
11
|
+
if (typeof s['html'] === 'string') parts.push((s['html'] as string).replace(/<[^>]+>/g, ' '));
|
|
12
|
+
if (typeof s['title'] === 'string') parts.push(s['title'] as string);
|
|
13
|
+
if (Array.isArray(s['items'])) {
|
|
14
|
+
for (const item of s['items'] as Record<string, unknown>[]) {
|
|
15
|
+
if (typeof item['label'] === 'string') parts.push(item['label']);
|
|
16
|
+
if (typeof item['value'] === 'string') parts.push(item['value']);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parts.join(' ');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function countWords(seo: SEOSection[]): number {
|
|
23
|
+
const fullText = seo.map(extractText).join(' ');
|
|
24
|
+
return fullText.split(/\s+/).filter((w) => w.length > 0).length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('SEO Content Length Validation', () => {
|
|
28
|
+
ALL_TOOLS.forEach((tool) => {
|
|
29
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
30
|
+
Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
|
|
31
|
+
it(`${locale}: SEO content should have at least ${MIN_WORDS} words`, async () => {
|
|
32
|
+
const content = await loader();
|
|
33
|
+
const wordCount = countWords(content.seo);
|
|
34
|
+
expect(wordCount, `Got ${wordCount} words, need ${MIN_WORDS}`).toBeGreaterThanOrEqual(MIN_WORDS);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import { alcoholCategory } from '../data';
|
|
4
|
+
|
|
5
|
+
describe('Tool Validation Suite', () => {
|
|
6
|
+
describe('Library Registration', () => {
|
|
7
|
+
it('should have 5 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(5);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('alcoholCategory should be defined', () => {
|
|
12
|
+
expect(alcoholCategory).toBeDefined();
|
|
13
|
+
expect(alcoholCategory.i18n).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|