@hagicode/hagilight 0.1.1 → 0.1.2-dev.10.1.aebe89d
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/Analytics51LA.astro +17 -0
- package/GoogleAnalytics.astro +21 -0
- package/PromotoBanner.astro +346 -0
- package/logo.png +0 -0
- package/package.json +11 -3
- package/promotions.ts +263 -0
- package/promoto-banner.ts +358 -0
- package/related-sites.json +262 -0
- package/site-links.ts +406 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { loadActivePromotions, type PromotionCard } from './promotions.ts';
|
|
2
|
+
|
|
3
|
+
const ELEMENT_NAME = 'hagilight-promoto-banner';
|
|
4
|
+
const DISMISSED_SIGNATURE_KEY = 'hagilight:promoto-banner:dismissed-signature';
|
|
5
|
+
const ROTATION_INTERVAL_MS = 8000;
|
|
6
|
+
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
export type PromotionVisibility = 'dismissed' | 'footer-hidden' | 'hidden' | 'ready';
|
|
9
|
+
|
|
10
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isSafeUrl(value: string): boolean {
|
|
15
|
+
try {
|
|
16
|
+
return ['http:', 'https:'].includes(new URL(value, 'https://hagilight.invalid/').protocol);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isUsableCard(value: unknown): value is PromotionCard {
|
|
23
|
+
if (!isRecord(value)) return false;
|
|
24
|
+
const image = value.image;
|
|
25
|
+
const imageIsValid = image === undefined || (
|
|
26
|
+
isRecord(image) && typeof image.src === 'string' && isSafeUrl(image.src)
|
|
27
|
+
&& typeof image.alt === 'string'
|
|
28
|
+
&& (image.variant === undefined || typeof image.variant === 'string')
|
|
29
|
+
&& (image.width === undefined || (typeof image.width === 'number' && Number.isFinite(image.width) && image.width > 0))
|
|
30
|
+
&& (image.height === undefined || (typeof image.height === 'number' && Number.isFinite(image.height) && image.height > 0))
|
|
31
|
+
);
|
|
32
|
+
return typeof value.id === 'string' && value.id.trim().length > 0
|
|
33
|
+
&& typeof value.title === 'string' && value.title.trim().length > 0
|
|
34
|
+
&& typeof value.description === 'string' && value.description.trim().length > 0
|
|
35
|
+
&& typeof value.ctaLabel === 'string' && value.ctaLabel.trim().length > 0
|
|
36
|
+
&& typeof value.link === 'string' && value.link.trim().length > 0 && isSafeUrl(value.link)
|
|
37
|
+
&& imageIsValid;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function selectPromotionCards(
|
|
41
|
+
remote: PromotionCard[],
|
|
42
|
+
fallback?: PromotionCard | null,
|
|
43
|
+
): PromotionCard[] {
|
|
44
|
+
const eligibleRemote = remote.filter(isUsableCard);
|
|
45
|
+
if (eligibleRemote.length) return eligibleRemote;
|
|
46
|
+
return fallback && isUsableCard(fallback) ? [fallback] : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getPromotionSetSignature(cards: PromotionCard[]): string | null {
|
|
50
|
+
if (!cards.length) return null;
|
|
51
|
+
return JSON.stringify(cards.map(({ id, title, description, ctaLabel, link, image }) => [
|
|
52
|
+
id, title, description, ctaLabel, link, image?.src ?? '', image?.alt ?? '',
|
|
53
|
+
image?.variant ?? '', image?.width ?? 0, image?.height ?? 0,
|
|
54
|
+
]));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getPromotionVisibility(
|
|
58
|
+
cards: PromotionCard[],
|
|
59
|
+
signature: string | null,
|
|
60
|
+
dismissedSignature: string | null,
|
|
61
|
+
footerVisible: boolean,
|
|
62
|
+
): PromotionVisibility {
|
|
63
|
+
if (!cards.length) return 'hidden';
|
|
64
|
+
if (signature && signature === dismissedSignature) return 'dismissed';
|
|
65
|
+
return footerVisible ? 'footer-hidden' : 'ready';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getNextPromotionIndex(index: number, count: number, direction: -1 | 1): number {
|
|
69
|
+
return count > 0 ? (index + direction + count) % count : 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function shouldAutoRotate(
|
|
73
|
+
count: number,
|
|
74
|
+
paused: boolean,
|
|
75
|
+
reducedMotion: boolean,
|
|
76
|
+
documentVisible: boolean,
|
|
77
|
+
visibility: PromotionVisibility,
|
|
78
|
+
): boolean {
|
|
79
|
+
return count > 1 && !paused && !reducedMotion && documentVisible && visibility === 'ready';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readDismissedSignature(): string | null {
|
|
83
|
+
try {
|
|
84
|
+
return window.localStorage.getItem(DISMISSED_SIGNATURE_KEY);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function saveDismissedSignature(signature: string): void {
|
|
91
|
+
try {
|
|
92
|
+
window.localStorage.setItem(DISMISSED_SIGNATURE_KEY, signature);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn('Unable to save the dismissed promotion preference.', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function definePromotoBannerElement(): void {
|
|
99
|
+
if (typeof customElements === 'undefined' || typeof HTMLElement === 'undefined'
|
|
100
|
+
|| customElements.get(ELEMENT_NAME)) return;
|
|
101
|
+
|
|
102
|
+
class PromotoBannerElement extends HTMLElement {
|
|
103
|
+
private cards: PromotionCard[] = [];
|
|
104
|
+
private currentIndex = 0;
|
|
105
|
+
private dismissedSignature = readDismissedSignature();
|
|
106
|
+
private paused = false;
|
|
107
|
+
private reducedMotion = false;
|
|
108
|
+
private rotationTimer: number | undefined;
|
|
109
|
+
private refreshTimer: number | undefined;
|
|
110
|
+
private motionQuery: MediaQueryList | undefined;
|
|
111
|
+
private resizeObserver: ResizeObserver | undefined;
|
|
112
|
+
private footer: HTMLElement | null = null;
|
|
113
|
+
|
|
114
|
+
private readonly onPrevious = () => this.move(-1);
|
|
115
|
+
private readonly onNext = () => this.move(1);
|
|
116
|
+
private readonly onPause = () => {
|
|
117
|
+
if (this.reducedMotion) return;
|
|
118
|
+
this.paused = !this.paused;
|
|
119
|
+
this.updateControls();
|
|
120
|
+
this.syncRotation();
|
|
121
|
+
};
|
|
122
|
+
private readonly onDismiss = () => {
|
|
123
|
+
const signature = getPromotionSetSignature(this.cards);
|
|
124
|
+
if (!signature) return;
|
|
125
|
+
this.dismissedSignature = signature;
|
|
126
|
+
saveDismissedSignature(signature);
|
|
127
|
+
this.updateVisibility();
|
|
128
|
+
this.stopRotation();
|
|
129
|
+
};
|
|
130
|
+
private readonly onLayoutChange = () => {
|
|
131
|
+
const previousState = this.dataset.state;
|
|
132
|
+
this.updateVisibility();
|
|
133
|
+
if (previousState !== this.dataset.state) this.syncRotation();
|
|
134
|
+
};
|
|
135
|
+
private readonly onVisibilityChange = () => {
|
|
136
|
+
if (document.visibilityState === 'hidden') {
|
|
137
|
+
this.stopRotation();
|
|
138
|
+
this.stopRefresh();
|
|
139
|
+
} else {
|
|
140
|
+
this.syncRotation();
|
|
141
|
+
this.startRefresh();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
private readonly onMotionChange = () => {
|
|
145
|
+
this.reducedMotion = this.motionQuery?.matches ?? false;
|
|
146
|
+
this.updateControls();
|
|
147
|
+
this.syncRotation();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
connectedCallback(): void {
|
|
151
|
+
this.footer = document.querySelector('footer');
|
|
152
|
+
this.motionQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
153
|
+
this.reducedMotion = this.motionQuery?.matches ?? false;
|
|
154
|
+
this.querySelector('[data-promoto-previous]')?.addEventListener('click', this.onPrevious);
|
|
155
|
+
this.querySelector('[data-promoto-next]')?.addEventListener('click', this.onNext);
|
|
156
|
+
this.querySelector('[data-promoto-pause]')?.addEventListener('click', this.onPause);
|
|
157
|
+
this.querySelector('[data-promoto-dismiss]')?.addEventListener('click', this.onDismiss);
|
|
158
|
+
window.addEventListener('scroll', this.onLayoutChange, { passive: true });
|
|
159
|
+
window.addEventListener('resize', this.onLayoutChange);
|
|
160
|
+
document.addEventListener('visibilitychange', this.onVisibilityChange);
|
|
161
|
+
this.motionQuery?.addEventListener('change', this.onMotionChange);
|
|
162
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
163
|
+
this.resizeObserver = new ResizeObserver(this.onLayoutChange);
|
|
164
|
+
if (this.footer) this.resizeObserver.observe(this.footer);
|
|
165
|
+
const shell = this.querySelector('[data-promoto-shell]');
|
|
166
|
+
if (shell) this.resizeObserver.observe(shell);
|
|
167
|
+
}
|
|
168
|
+
void this.reload();
|
|
169
|
+
this.startRefresh();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
disconnectedCallback(): void {
|
|
173
|
+
this.querySelector('[data-promoto-previous]')?.removeEventListener('click', this.onPrevious);
|
|
174
|
+
this.querySelector('[data-promoto-next]')?.removeEventListener('click', this.onNext);
|
|
175
|
+
this.querySelector('[data-promoto-pause]')?.removeEventListener('click', this.onPause);
|
|
176
|
+
this.querySelector('[data-promoto-dismiss]')?.removeEventListener('click', this.onDismiss);
|
|
177
|
+
window.removeEventListener('scroll', this.onLayoutChange);
|
|
178
|
+
window.removeEventListener('resize', this.onLayoutChange);
|
|
179
|
+
document.removeEventListener('visibilitychange', this.onVisibilityChange);
|
|
180
|
+
this.motionQuery?.removeEventListener('change', this.onMotionChange);
|
|
181
|
+
this.resizeObserver?.disconnect();
|
|
182
|
+
this.stopRotation();
|
|
183
|
+
this.stopRefresh();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async reload(): Promise<void> {
|
|
187
|
+
const previousId = this.cards[this.currentIndex]?.id;
|
|
188
|
+
const remote = await loadActivePromotions({ locale: this.dataset.locale });
|
|
189
|
+
if (!this.isConnected) return;
|
|
190
|
+
const fallback = this.readFallback();
|
|
191
|
+
this.cards = selectPromotionCards(remote, fallback);
|
|
192
|
+
const previousIndex = previousId ? this.cards.findIndex((card) => card.id === previousId) : -1;
|
|
193
|
+
this.currentIndex = previousIndex >= 0 ? previousIndex : 0;
|
|
194
|
+
this.renderCards();
|
|
195
|
+
this.updateVisibility();
|
|
196
|
+
this.updateControls();
|
|
197
|
+
this.syncRotation();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private readFallback(): PromotionCard | null {
|
|
201
|
+
const serialized = this.dataset.fallback;
|
|
202
|
+
if (!serialized) return null;
|
|
203
|
+
try {
|
|
204
|
+
const value: unknown = JSON.parse(serialized);
|
|
205
|
+
return isUsableCard(value) ? value : null;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn('Unable to parse the site-provided promotion fallback.', error);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private renderCards(): void {
|
|
213
|
+
const track = this.querySelector<HTMLElement>('[data-promoto-track]');
|
|
214
|
+
if (!track) return;
|
|
215
|
+
const fragment = document.createDocumentFragment();
|
|
216
|
+
this.cards.forEach((card, index) => {
|
|
217
|
+
const slide = document.createElement('article');
|
|
218
|
+
slide.className = 'hagilight-promoto__slide';
|
|
219
|
+
slide.dataset.hasImage = String(Boolean(card.image));
|
|
220
|
+
slide.setAttribute('aria-hidden', index === this.currentIndex ? 'false' : 'true');
|
|
221
|
+
if (index !== this.currentIndex) slide.setAttribute('inert', '');
|
|
222
|
+
|
|
223
|
+
const copy = document.createElement('div');
|
|
224
|
+
copy.className = 'hagilight-promoto__copy';
|
|
225
|
+
const title = document.createElement('h2');
|
|
226
|
+
title.className = 'hagilight-promoto__title';
|
|
227
|
+
title.textContent = card.title;
|
|
228
|
+
const description = document.createElement('p');
|
|
229
|
+
description.className = 'hagilight-promoto__description';
|
|
230
|
+
description.textContent = card.description;
|
|
231
|
+
copy.append(title, description);
|
|
232
|
+
|
|
233
|
+
const link = document.createElement('a');
|
|
234
|
+
link.className = 'hagilight-promoto__cta';
|
|
235
|
+
link.href = card.link;
|
|
236
|
+
link.target = '_blank';
|
|
237
|
+
link.rel = 'noopener noreferrer';
|
|
238
|
+
link.textContent = card.ctaLabel;
|
|
239
|
+
slide.append(copy);
|
|
240
|
+
|
|
241
|
+
if (card.image) {
|
|
242
|
+
const media = document.createElement('span');
|
|
243
|
+
media.className = 'hagilight-promoto__media';
|
|
244
|
+
const image = document.createElement('img');
|
|
245
|
+
image.className = 'hagilight-promoto__image';
|
|
246
|
+
image.src = card.image.src;
|
|
247
|
+
image.alt = card.image.alt || card.title;
|
|
248
|
+
image.loading = index === this.currentIndex ? 'eager' : 'lazy';
|
|
249
|
+
image.decoding = 'async';
|
|
250
|
+
if (card.image.width) image.width = card.image.width;
|
|
251
|
+
if (card.image.height) image.height = card.image.height;
|
|
252
|
+
media.append(image);
|
|
253
|
+
slide.append(media);
|
|
254
|
+
}
|
|
255
|
+
slide.append(link);
|
|
256
|
+
fragment.append(slide);
|
|
257
|
+
});
|
|
258
|
+
track.replaceChildren(fragment);
|
|
259
|
+
this.updateSlides();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private updateSlides(): void {
|
|
263
|
+
const track = this.querySelector<HTMLElement>('[data-promoto-track]');
|
|
264
|
+
if (!track) return;
|
|
265
|
+
Array.from(track.children).forEach((slide, index) => {
|
|
266
|
+
const active = index === this.currentIndex;
|
|
267
|
+
slide.setAttribute('aria-hidden', String(!active));
|
|
268
|
+
if (active) slide.removeAttribute('inert');
|
|
269
|
+
else slide.setAttribute('inert', '');
|
|
270
|
+
});
|
|
271
|
+
track.style.transform = `translateX(-${this.currentIndex * 100}%)`;
|
|
272
|
+
const current = this.cards[this.currentIndex];
|
|
273
|
+
const status = this.querySelector<HTMLElement>('[data-promoto-status]');
|
|
274
|
+
if (status) status.textContent = current?.title ?? '';
|
|
275
|
+
const count = this.querySelector<HTMLElement>('[data-promoto-count]');
|
|
276
|
+
if (count) count.textContent = `${this.currentIndex + 1} / ${this.cards.length}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private updateControls(): void {
|
|
280
|
+
const multiple = this.cards.length > 1;
|
|
281
|
+
const controls = this.querySelector<HTMLElement>('[data-promoto-controls]');
|
|
282
|
+
if (controls) controls.hidden = !multiple;
|
|
283
|
+
const pause = this.querySelector<HTMLButtonElement>('[data-promoto-pause]');
|
|
284
|
+
if (pause) {
|
|
285
|
+
const paused = this.paused || this.reducedMotion;
|
|
286
|
+
pause.disabled = this.reducedMotion;
|
|
287
|
+
pause.setAttribute('aria-pressed', String(paused));
|
|
288
|
+
pause.setAttribute('aria-label', paused ? 'Resume automatic promotion rotation' : 'Pause automatic promotion rotation');
|
|
289
|
+
pause.textContent = paused ? 'Resume' : 'Pause';
|
|
290
|
+
}
|
|
291
|
+
this.updateSlides();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private move(direction: -1 | 1): void {
|
|
295
|
+
if (this.cards.length < 2) return;
|
|
296
|
+
this.currentIndex = getNextPromotionIndex(this.currentIndex, this.cards.length, direction);
|
|
297
|
+
this.updateSlides();
|
|
298
|
+
this.syncRotation();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private updateVisibility(): void {
|
|
302
|
+
const signature = getPromotionSetSignature(this.cards);
|
|
303
|
+
const state = getPromotionVisibility(
|
|
304
|
+
this.cards,
|
|
305
|
+
signature,
|
|
306
|
+
this.dismissedSignature,
|
|
307
|
+
this.isFooterVisible(),
|
|
308
|
+
);
|
|
309
|
+
const hidden = state === 'hidden' || state === 'dismissed';
|
|
310
|
+
const shell = this.querySelector<HTMLElement>('[data-promoto-shell]');
|
|
311
|
+
this.hidden = hidden;
|
|
312
|
+
if (shell) {
|
|
313
|
+
shell.hidden = hidden;
|
|
314
|
+
shell.inert = hidden || state === 'footer-hidden';
|
|
315
|
+
}
|
|
316
|
+
this.dataset.state = state;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private isFooterVisible(): boolean {
|
|
320
|
+
if (!this.footer) return false;
|
|
321
|
+
const bounds = this.footer.getBoundingClientRect();
|
|
322
|
+
return bounds.top < window.innerHeight && bounds.bottom > 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private syncRotation(): void {
|
|
326
|
+
this.stopRotation();
|
|
327
|
+
const state = this.dataset.state;
|
|
328
|
+
if (state !== 'ready' && state !== 'footer-hidden' && state !== 'hidden' && state !== 'dismissed') return;
|
|
329
|
+
if (!shouldAutoRotate(
|
|
330
|
+
this.cards.length,
|
|
331
|
+
this.paused,
|
|
332
|
+
this.reducedMotion,
|
|
333
|
+
document.visibilityState !== 'hidden',
|
|
334
|
+
state,
|
|
335
|
+
)) return;
|
|
336
|
+
this.rotationTimer = window.setInterval(() => this.move(1), ROTATION_INTERVAL_MS);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private stopRotation(): void {
|
|
340
|
+
if (this.rotationTimer !== undefined) window.clearInterval(this.rotationTimer);
|
|
341
|
+
this.rotationTimer = undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private startRefresh(): void {
|
|
345
|
+
this.stopRefresh();
|
|
346
|
+
if (document.visibilityState !== 'hidden') {
|
|
347
|
+
this.refreshTimer = window.setInterval(() => void this.reload(), REFRESH_INTERVAL_MS);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private stopRefresh(): void {
|
|
352
|
+
if (this.refreshTimer !== undefined) window.clearInterval(this.refreshTimer);
|
|
353
|
+
this.refreshTimer = undefined;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
customElements.define(ELEMENT_NAME, PromotoBannerElement);
|
|
358
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "hagicode-main",
|
|
4
|
+
"name": {
|
|
5
|
+
"zh-CN": "HagiCode 主站",
|
|
6
|
+
"zh-Hant": "HagiCode 主站",
|
|
7
|
+
"en-US": "HagiCode Main Site",
|
|
8
|
+
"ja-JP": "HagiCode 公式サイト",
|
|
9
|
+
"ko-KR": "HagiCode 메인 사이트",
|
|
10
|
+
"de-DE": "HagiCode Hauptseite",
|
|
11
|
+
"fr-FR": "Site principal HagiCode",
|
|
12
|
+
"es-ES": "Sitio principal de HagiCode",
|
|
13
|
+
"pt-BR": "Site principal da HagiCode",
|
|
14
|
+
"ru-RU": "Главный сайт HagiCode"
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"zh-CN": "产品入口",
|
|
18
|
+
"zh-Hant": "產品入口",
|
|
19
|
+
"en-US": "Primary product entry.",
|
|
20
|
+
"ja-JP": "製品全体の入口です。",
|
|
21
|
+
"ko-KR": "제품으로 들어가는 기본 진입점입니다.",
|
|
22
|
+
"de-DE": "Zentraler Einstieg zum Produkt.",
|
|
23
|
+
"fr-FR": "Point d'entrée principal du produit.",
|
|
24
|
+
"es-ES": "Entrada principal al producto.",
|
|
25
|
+
"pt-BR": "Entrada principal do produto.",
|
|
26
|
+
"ru-RU": "Основной вход в продукт."
|
|
27
|
+
},
|
|
28
|
+
"url": "https://www.hagicode.com/",
|
|
29
|
+
"supportsLocalePath": true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "newbe-blog",
|
|
33
|
+
"name": {
|
|
34
|
+
"zh-CN": "newbe",
|
|
35
|
+
"zh-Hant": "newbe",
|
|
36
|
+
"en-US": "newbe",
|
|
37
|
+
"ja-JP": "newbe",
|
|
38
|
+
"ko-KR": "newbe",
|
|
39
|
+
"de-DE": "newbe",
|
|
40
|
+
"fr-FR": "newbe",
|
|
41
|
+
"es-ES": "newbe",
|
|
42
|
+
"pt-BR": "newbe",
|
|
43
|
+
"ru-RU": "newbe"
|
|
44
|
+
},
|
|
45
|
+
"description": {
|
|
46
|
+
"zh-CN": "作者博客",
|
|
47
|
+
"zh-Hant": "作者部落格",
|
|
48
|
+
"en-US": "Author blog.",
|
|
49
|
+
"ja-JP": "作者ブログです。",
|
|
50
|
+
"ko-KR": "작성자 블로그입니다.",
|
|
51
|
+
"de-DE": "Blog des Autors.",
|
|
52
|
+
"fr-FR": "Blog de l'auteur.",
|
|
53
|
+
"es-ES": "Blog del autor.",
|
|
54
|
+
"pt-BR": "Blog do autor.",
|
|
55
|
+
"ru-RU": "Блог автора."
|
|
56
|
+
},
|
|
57
|
+
"url": "https://newbe.hagicode.com/"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "index-data",
|
|
61
|
+
"name": {
|
|
62
|
+
"zh-CN": "Index Data Mirror",
|
|
63
|
+
"zh-Hant": "Index Data Mirror",
|
|
64
|
+
"en-US": "Index Data Mirror",
|
|
65
|
+
"ja-JP": "Index Data Mirror",
|
|
66
|
+
"ko-KR": "Index Data Mirror",
|
|
67
|
+
"de-DE": "Index Data Mirror",
|
|
68
|
+
"fr-FR": "Index Data Mirror",
|
|
69
|
+
"es-ES": "Index Data Mirror",
|
|
70
|
+
"pt-BR": "Index Data Mirror",
|
|
71
|
+
"ru-RU": "Index Data Mirror"
|
|
72
|
+
},
|
|
73
|
+
"description": {
|
|
74
|
+
"zh-CN": "公开数据",
|
|
75
|
+
"zh-Hant": "公開資料",
|
|
76
|
+
"en-US": "Public data mirror.",
|
|
77
|
+
"ja-JP": "公開データミラーです。",
|
|
78
|
+
"ko-KR": "공개 데이터 미러입니다.",
|
|
79
|
+
"de-DE": "Öffentlicher Daten-Spiegel.",
|
|
80
|
+
"fr-FR": "Miroir de données public.",
|
|
81
|
+
"es-ES": "Espejo de datos público.",
|
|
82
|
+
"pt-BR": "Espelho público de dados.",
|
|
83
|
+
"ru-RU": "Публичное зеркало данных."
|
|
84
|
+
},
|
|
85
|
+
"url": "https://index.hagicode.com/data/"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "compose-builder",
|
|
89
|
+
"name": {
|
|
90
|
+
"zh-CN": "Docker Compose Builder",
|
|
91
|
+
"zh-Hant": "Docker Compose Builder",
|
|
92
|
+
"en-US": "Docker Compose Builder",
|
|
93
|
+
"ja-JP": "Docker Compose Builder",
|
|
94
|
+
"ko-KR": "Docker Compose Builder",
|
|
95
|
+
"de-DE": "Docker Compose Builder",
|
|
96
|
+
"fr-FR": "Docker Compose Builder",
|
|
97
|
+
"es-ES": "Docker Compose Builder",
|
|
98
|
+
"pt-BR": "Docker Compose Builder",
|
|
99
|
+
"ru-RU": "Docker Compose Builder"
|
|
100
|
+
},
|
|
101
|
+
"description": {
|
|
102
|
+
"zh-CN": "Docker 部署 Hagicode",
|
|
103
|
+
"zh-Hant": "使用 Docker 部署 Hagicode",
|
|
104
|
+
"en-US": "Deploy HagiCode with Docker.",
|
|
105
|
+
"ja-JP": "Docker で HagiCode をデプロイします。",
|
|
106
|
+
"ko-KR": "Docker로 HagiCode를 배포합니다.",
|
|
107
|
+
"de-DE": "HagiCode mit Docker bereitstellen.",
|
|
108
|
+
"fr-FR": "Déployer HagiCode avec Docker.",
|
|
109
|
+
"es-ES": "Despliega HagiCode con Docker.",
|
|
110
|
+
"pt-BR": "Implante a HagiCode com Docker.",
|
|
111
|
+
"ru-RU": "Развёртывание HagiCode через Docker."
|
|
112
|
+
},
|
|
113
|
+
"url": "https://builder.hagicode.com/"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "status-page",
|
|
117
|
+
"name": {
|
|
118
|
+
"zh-CN": "HagiCode Status",
|
|
119
|
+
"zh-Hant": "HagiCode Status",
|
|
120
|
+
"en-US": "HagiCode Status",
|
|
121
|
+
"ja-JP": "HagiCode Status",
|
|
122
|
+
"ko-KR": "HagiCode Status",
|
|
123
|
+
"de-DE": "HagiCode Status",
|
|
124
|
+
"fr-FR": "HagiCode Status",
|
|
125
|
+
"es-ES": "HagiCode Status",
|
|
126
|
+
"pt-BR": "HagiCode Status",
|
|
127
|
+
"ru-RU": "HagiCode Status"
|
|
128
|
+
},
|
|
129
|
+
"description": {
|
|
130
|
+
"zh-CN": "服务状态",
|
|
131
|
+
"zh-Hant": "服務狀態",
|
|
132
|
+
"en-US": "Service status.",
|
|
133
|
+
"ja-JP": "サービス状態の確認ページです。",
|
|
134
|
+
"ko-KR": "서비스 상태를 확인하는 페이지입니다.",
|
|
135
|
+
"de-DE": "Dienststatus.",
|
|
136
|
+
"fr-FR": "État des services.",
|
|
137
|
+
"es-ES": "Estado del servicio.",
|
|
138
|
+
"pt-BR": "Status do serviço.",
|
|
139
|
+
"ru-RU": "Статус сервисов."
|
|
140
|
+
},
|
|
141
|
+
"url": "https://status.hagicode.com/"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "awesome-design-gallery",
|
|
145
|
+
"name": {
|
|
146
|
+
"zh-CN": "Awesome Design MD",
|
|
147
|
+
"zh-Hant": "Awesome Design MD",
|
|
148
|
+
"en-US": "Awesome Design MD",
|
|
149
|
+
"ja-JP": "Awesome Design MD",
|
|
150
|
+
"ko-KR": "Awesome Design MD",
|
|
151
|
+
"de-DE": "Awesome Design MD",
|
|
152
|
+
"fr-FR": "Awesome Design MD",
|
|
153
|
+
"es-ES": "Awesome Design MD",
|
|
154
|
+
"pt-BR": "Awesome Design MD",
|
|
155
|
+
"ru-RU": "Awesome Design MD"
|
|
156
|
+
},
|
|
157
|
+
"description": {
|
|
158
|
+
"zh-CN": "超酷的AI Web 设计",
|
|
159
|
+
"zh-Hant": "超酷的 AI Web 設計",
|
|
160
|
+
"en-US": "A gallery of standout AI web design work.",
|
|
161
|
+
"ja-JP": "印象的な AI Web デザインを集めたギャラリーです。",
|
|
162
|
+
"ko-KR": "인상적인 AI 웹 디자인을 모아 둔 갤러리입니다.",
|
|
163
|
+
"de-DE": "Galerie mit markanten AI-Webdesigns.",
|
|
164
|
+
"fr-FR": "Galerie de designs web IA remarquables.",
|
|
165
|
+
"es-ES": "Galería de diseños web de IA destacados.",
|
|
166
|
+
"pt-BR": "Galeria com designs web de IA de destaque.",
|
|
167
|
+
"ru-RU": "Галерея выразительных AI web-дизайнов."
|
|
168
|
+
},
|
|
169
|
+
"url": "https://design.hagicode.com/",
|
|
170
|
+
"supportsLocalePath": true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "soul-builder",
|
|
174
|
+
"name": {
|
|
175
|
+
"zh-CN": "Soul Builder",
|
|
176
|
+
"zh-Hant": "Soul Builder",
|
|
177
|
+
"en-US": "Soul Builder",
|
|
178
|
+
"ja-JP": "Soul Builder",
|
|
179
|
+
"ko-KR": "Soul Builder",
|
|
180
|
+
"de-DE": "Soul Builder",
|
|
181
|
+
"fr-FR": "Soul Builder",
|
|
182
|
+
"es-ES": "Soul Builder",
|
|
183
|
+
"pt-BR": "Soul Builder",
|
|
184
|
+
"ru-RU": "Soul Builder"
|
|
185
|
+
},
|
|
186
|
+
"description": {
|
|
187
|
+
"zh-CN": "Agent之魂",
|
|
188
|
+
"zh-Hant": "Agent 之魂",
|
|
189
|
+
"en-US": "Define the soul of an agent.",
|
|
190
|
+
"ja-JP": "Agent の魂を形にする体験です。",
|
|
191
|
+
"ko-KR": "에이전트의 영혼을 설계하는 경험입니다.",
|
|
192
|
+
"de-DE": "Forme die Seele eines Agenten.",
|
|
193
|
+
"fr-FR": "Façonnez l'âme d'un agent.",
|
|
194
|
+
"es-ES": "Define el alma de un agente.",
|
|
195
|
+
"pt-BR": "Defina a alma de um agente.",
|
|
196
|
+
"ru-RU": "Определите душу агента."
|
|
197
|
+
},
|
|
198
|
+
"url": "https://soul.hagicode.com/"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "trait-builder",
|
|
202
|
+
"name": {
|
|
203
|
+
"zh-CN": "Trait Builder",
|
|
204
|
+
"zh-Hant": "Trait Builder",
|
|
205
|
+
"en-US": "Trait Builder",
|
|
206
|
+
"ja-JP": "Trait Builder",
|
|
207
|
+
"ko-KR": "Trait Builder",
|
|
208
|
+
"de-DE": "Trait Builder",
|
|
209
|
+
"fr-FR": "Trait Builder",
|
|
210
|
+
"es-ES": "Trait Builder",
|
|
211
|
+
"pt-BR": "Trait Builder",
|
|
212
|
+
"ru-RU": "Trait Builder"
|
|
213
|
+
},
|
|
214
|
+
"description": {
|
|
215
|
+
"zh-CN": "Agent之能",
|
|
216
|
+
"zh-Hant": "Agent 之能",
|
|
217
|
+
"en-US": "Shape what an agent can do.",
|
|
218
|
+
"ja-JP": "Agent の能力を組み立てる体験です。",
|
|
219
|
+
"ko-KR": "에이전트의 능력을 설계하는 경험입니다.",
|
|
220
|
+
"de-DE": "Definiere, was ein Agent kann.",
|
|
221
|
+
"fr-FR": "Définissez ce qu'un agent peut faire.",
|
|
222
|
+
"es-ES": "Define lo que un agente puede hacer.",
|
|
223
|
+
"pt-BR": "Defina o que um agente pode fazer.",
|
|
224
|
+
"ru-RU": "Соберите способности агента."
|
|
225
|
+
},
|
|
226
|
+
"url": "https://trait.hagicode.com/"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"id": "openspec-docs",
|
|
230
|
+
"name": {
|
|
231
|
+
"zh-CN": "OpenSpec 文档",
|
|
232
|
+
"zh-Hant": "OpenSpec 文件",
|
|
233
|
+
"en-US": "OpenSpec Docs",
|
|
234
|
+
"ja-JP": "OpenSpec ドキュメント",
|
|
235
|
+
"ko-KR": "OpenSpec 문서",
|
|
236
|
+
"de-DE": "OpenSpec-Dokumentation",
|
|
237
|
+
"fr-FR": "Documentation OpenSpec",
|
|
238
|
+
"es-ES": "Documentación de OpenSpec",
|
|
239
|
+
"pt-BR": "Documentação do OpenSpec",
|
|
240
|
+
"ru-RU": "Документация OpenSpec"
|
|
241
|
+
},
|
|
242
|
+
"url": "https://openspec.hagicode.com/",
|
|
243
|
+
"supportsLocalePath": true
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"id": "omniroute-docs",
|
|
247
|
+
"name": {
|
|
248
|
+
"zh-CN": "OmniRoute 文档",
|
|
249
|
+
"zh-Hant": "OmniRoute 文件",
|
|
250
|
+
"en-US": "OmniRoute Docs",
|
|
251
|
+
"ja-JP": "OmniRoute ドキュメント",
|
|
252
|
+
"ko-KR": "OmniRoute 문서",
|
|
253
|
+
"de-DE": "OmniRoute-Dokumentation",
|
|
254
|
+
"fr-FR": "Documentation OmniRoute",
|
|
255
|
+
"es-ES": "Documentación de OmniRoute",
|
|
256
|
+
"pt-BR": "Documentação do OmniRoute",
|
|
257
|
+
"ru-RU": "Документация OmniRoute"
|
|
258
|
+
},
|
|
259
|
+
"url": "https://omniroute.hagicode.com/",
|
|
260
|
+
"supportsLocalePath": true
|
|
261
|
+
}
|
|
262
|
+
]
|