@seip/blue-bird 0.3.8 → 0.4.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/backend/routes/frontend.js +3 -20
- package/core/cli/react.js +187 -135
- package/core/cli/scaffolding-auth.js +27 -11
- package/core/template.js +195 -4
- package/frontend/index.html +4 -7
- package/frontend/resources/css/tailwind.css +2 -0
- package/frontend/resources/js/App.jsx +16 -9
- package/frontend/resources/js/Main.jsx +12 -11
- package/frontend/resources/js/blue-bird/components/Card.jsx +3 -2
- package/frontend/resources/js/blue-bird/components/Skeleton.jsx +45 -0
- package/frontend/resources/js/blue-bird/contexts/LanguageContext.jsx +12 -1
- package/frontend/resources/js/blue-bird/locales/en.json +18 -0
- package/frontend/resources/js/blue-bird/locales/es.json +18 -0
- package/frontend/resources/js/components/Header.jsx +53 -0
- package/frontend/resources/js/pages/About.jsx +21 -23
- package/frontend/resources/js/pages/Home.jsx +47 -58
- package/package.json +3 -1
- package/vite.config.js +3 -2
package/core/template.js
CHANGED
|
@@ -91,6 +91,9 @@ class Template {
|
|
|
91
91
|
*
|
|
92
92
|
* @param {string} [options.metaTags.langMeta]
|
|
93
93
|
* Alternative language metadata value.
|
|
94
|
+
*
|
|
95
|
+
* @param {boolean} [options.skeleton=true]
|
|
96
|
+
* Enables skeleton loading.
|
|
94
97
|
*
|
|
95
98
|
* @returns {void}
|
|
96
99
|
* Sends a complete HTML response to the client.
|
|
@@ -120,7 +123,8 @@ class Template {
|
|
|
120
123
|
* keywordsMeta: "express, react, framework",
|
|
121
124
|
* authorMeta: "Blue Bird",
|
|
122
125
|
* langMeta: "en"
|
|
123
|
-
* }
|
|
126
|
+
* },
|
|
127
|
+
* skeleton: true
|
|
124
128
|
* };
|
|
125
129
|
*
|
|
126
130
|
* Template.renderReact(res, "App", { title: "Hello World" }, options);
|
|
@@ -135,7 +139,8 @@ class Template {
|
|
|
135
139
|
scriptsInHead = [],
|
|
136
140
|
scriptsInBody = [],
|
|
137
141
|
cache = true,
|
|
138
|
-
metaTags
|
|
142
|
+
metaTags,
|
|
143
|
+
skeleton = true
|
|
139
144
|
} = options;
|
|
140
145
|
const metaTagsDefault = {
|
|
141
146
|
titleMeta: props.titleMeta,
|
|
@@ -177,12 +182,20 @@ class Template {
|
|
|
177
182
|
.map(item => `<script src="${item.src}"></script>`)
|
|
178
183
|
.join("");
|
|
179
184
|
|
|
180
|
-
|
|
185
|
+
let scriptsBodyTags = scriptsInBody
|
|
181
186
|
.map(item => `<script src="${item.src}"></script>`)
|
|
182
187
|
.join("");
|
|
183
188
|
|
|
184
189
|
const propsJson = JSON.stringify(componentProps).replace(/'/g, "'");
|
|
185
190
|
|
|
191
|
+
const stylesSkeleton = skeleton
|
|
192
|
+
? `<style>${this.skeletonStyles()}</style>`
|
|
193
|
+
: "";
|
|
194
|
+
|
|
195
|
+
const skeletonHtml = skeleton
|
|
196
|
+
? this.skeletonHtml()
|
|
197
|
+
: "";
|
|
198
|
+
|
|
186
199
|
let html = BASE_TEMPLATE
|
|
187
200
|
.replace(/__LANG__/g, this.escapeHtml(langHtml))
|
|
188
201
|
.replace(/__TITLE__/g, title)
|
|
@@ -196,7 +209,9 @@ class Template {
|
|
|
196
209
|
.replace(/__COMPONENT__/g, component)
|
|
197
210
|
.replace(/__PROPS__/g, propsJson)
|
|
198
211
|
.replace(/__VITE_ASSETS__/g, this.vite_assets())
|
|
199
|
-
.replace(/__SCRIPTS_BODY__/g, scriptsBodyTags)
|
|
212
|
+
.replace(/__SCRIPTS_BODY__/g, scriptsBodyTags)
|
|
213
|
+
.replace(/__STYLES_SKELETON__/g, stylesSkeleton)
|
|
214
|
+
.replace(/__SKELETON__/g, skeletonHtml);
|
|
200
215
|
|
|
201
216
|
html = this.minifyHtml(html);
|
|
202
217
|
CACHE_TEMPLATE[cacheKey] = html;
|
|
@@ -287,6 +302,182 @@ window.__vite_plugin_react_preamble_installed__ = true;
|
|
|
287
302
|
.replace(/"/g, """)
|
|
288
303
|
.replace(/'/g, "'");
|
|
289
304
|
}
|
|
305
|
+
static skeletonStyles() {
|
|
306
|
+
return `
|
|
307
|
+
@keyframes sk-pulse {
|
|
308
|
+
0%, 100% { opacity: 1; }
|
|
309
|
+
50% { opacity: 0.5; }
|
|
310
|
+
}
|
|
311
|
+
.sk-animate-pulse {
|
|
312
|
+
animation: sk-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
313
|
+
}
|
|
314
|
+
.sk-container {
|
|
315
|
+
min-height: 100vh;
|
|
316
|
+
width: 100%;
|
|
317
|
+
background-color: #f9fafb; /* bg-gray-50 */
|
|
318
|
+
padding: 1rem;
|
|
319
|
+
box-sizing: border-box;
|
|
320
|
+
}
|
|
321
|
+
.sk-inner {
|
|
322
|
+
display: flex;
|
|
323
|
+
flex-direction: column;
|
|
324
|
+
gap: 1.5rem;
|
|
325
|
+
}
|
|
326
|
+
.sk-header {
|
|
327
|
+
display: flex;
|
|
328
|
+
align-items: center;
|
|
329
|
+
justify-content: space-between;
|
|
330
|
+
width: 100%;
|
|
331
|
+
margin-bottom: 1rem;
|
|
332
|
+
}
|
|
333
|
+
.sk-btn-text {
|
|
334
|
+
height: 2.5rem;
|
|
335
|
+
width: 8rem;
|
|
336
|
+
background-color: #d1d5db; /* bg-gray-300 */
|
|
337
|
+
border-radius: 0.5rem;
|
|
338
|
+
}
|
|
339
|
+
.sk-header-actions {
|
|
340
|
+
display: flex;
|
|
341
|
+
gap: 1rem;
|
|
342
|
+
}
|
|
343
|
+
.sk-avatar {
|
|
344
|
+
height: 2.5rem;
|
|
345
|
+
width: 2.5rem;
|
|
346
|
+
background-color: #d1d5db;
|
|
347
|
+
border-radius: 9999px;
|
|
348
|
+
}
|
|
349
|
+
.sk-btn {
|
|
350
|
+
height: 2.5rem;
|
|
351
|
+
width: 6rem;
|
|
352
|
+
background-color: #d1d5db;
|
|
353
|
+
border-radius: 0.5rem;
|
|
354
|
+
}
|
|
355
|
+
.sk-hero {
|
|
356
|
+
height: 12rem;
|
|
357
|
+
width: 100%;
|
|
358
|
+
background-color: #d1d5db;
|
|
359
|
+
border-radius: 1rem;
|
|
360
|
+
}
|
|
361
|
+
.sk-grid {
|
|
362
|
+
display: grid;
|
|
363
|
+
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
364
|
+
gap: 1.5rem;
|
|
365
|
+
}
|
|
366
|
+
.sk-card {
|
|
367
|
+
display: flex;
|
|
368
|
+
flex-direction: column;
|
|
369
|
+
gap: 0.75rem;
|
|
370
|
+
}
|
|
371
|
+
.sk-card-img {
|
|
372
|
+
height: 10rem;
|
|
373
|
+
width: 100%;
|
|
374
|
+
background-color: #d1d5db;
|
|
375
|
+
border-radius: 0.75rem;
|
|
376
|
+
}
|
|
377
|
+
.sk-text-34 {
|
|
378
|
+
height: 1rem;
|
|
379
|
+
width: 75%;
|
|
380
|
+
background-color: #d1d5db;
|
|
381
|
+
border-radius: 0.25rem;
|
|
382
|
+
}
|
|
383
|
+
.sk-text-12 {
|
|
384
|
+
height: 1rem;
|
|
385
|
+
width: 50%;
|
|
386
|
+
background-color: #d1d5db;
|
|
387
|
+
border-radius: 0.25rem;
|
|
388
|
+
}
|
|
389
|
+
.sk-footer {
|
|
390
|
+
display: flex;
|
|
391
|
+
flex-direction: column;
|
|
392
|
+
gap: 0.5rem;
|
|
393
|
+
margin-top: 1rem;
|
|
394
|
+
}
|
|
395
|
+
.sk-text-full {
|
|
396
|
+
height: 1rem;
|
|
397
|
+
width: 100%;
|
|
398
|
+
background-color: #e5e7eb; /* bg-gray-200 */
|
|
399
|
+
border-radius: 0.25rem;
|
|
400
|
+
}
|
|
401
|
+
.sk-text-23 {
|
|
402
|
+
height: 1rem;
|
|
403
|
+
width: 66.666667%;
|
|
404
|
+
background-color: #e5e7eb;
|
|
405
|
+
border-radius: 0.25rem;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
@media (min-width: 768px) {
|
|
409
|
+
.sk-container {
|
|
410
|
+
padding: 2rem;
|
|
411
|
+
}
|
|
412
|
+
.sk-hero {
|
|
413
|
+
height: 16rem;
|
|
414
|
+
}
|
|
415
|
+
.sk-grid {
|
|
416
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
html.dark .sk-container {
|
|
421
|
+
background-color: #0b0f19; /* bg-gray-950/900 */
|
|
422
|
+
}
|
|
423
|
+
html.dark .sk-btn-text,
|
|
424
|
+
html.dark .sk-avatar,
|
|
425
|
+
html.dark .sk-btn,
|
|
426
|
+
html.dark .sk-hero,
|
|
427
|
+
html.dark .sk-card-img,
|
|
428
|
+
html.dark .sk-text-34,
|
|
429
|
+
html.dark .sk-text-12 {
|
|
430
|
+
background-color: #374151; /* bg-gray-700 */
|
|
431
|
+
}
|
|
432
|
+
html.dark .sk-text-full,
|
|
433
|
+
html.dark .sk-text-23 {
|
|
434
|
+
background-color: #1f2937; /* bg-gray-800 */
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
static skeletonHtml() {
|
|
440
|
+
return `
|
|
441
|
+
<div class="sk-container">
|
|
442
|
+
<div class="sk-inner sk-animate-pulse">
|
|
443
|
+
<div class="sk-header">
|
|
444
|
+
<div class="sk-btn-text"></div>
|
|
445
|
+
<div class="sk-header-actions">
|
|
446
|
+
<div class="sk-avatar"></div>
|
|
447
|
+
<div class="sk-btn"></div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<div class="sk-hero"></div>
|
|
452
|
+
|
|
453
|
+
<div class="sk-grid">
|
|
454
|
+
<div class="sk-card">
|
|
455
|
+
<div class="sk-card-img"></div>
|
|
456
|
+
<div class="sk-text-34"></div>
|
|
457
|
+
<div class="sk-text-12"></div>
|
|
458
|
+
</div>
|
|
459
|
+
<div class="sk-card">
|
|
460
|
+
<div class="sk-card-img"></div>
|
|
461
|
+
<div class="sk-text-34"></div>
|
|
462
|
+
<div class="sk-text-12"></div>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="sk-card">
|
|
465
|
+
<div class="sk-card-img"></div>
|
|
466
|
+
<div class="sk-text-34"></div>
|
|
467
|
+
<div class="sk-text-12"></div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<div class="sk-footer">
|
|
472
|
+
<div class="sk-text-full"></div>
|
|
473
|
+
<div class="sk-text-full"></div>
|
|
474
|
+
<div class="sk-text-23"></div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
|
|
290
481
|
}
|
|
291
482
|
|
|
292
483
|
export default Template;
|
package/frontend/index.html
CHANGED
|
@@ -13,16 +13,13 @@
|
|
|
13
13
|
__LINK_STYLES__
|
|
14
14
|
__SCRIPTS_HEAD__
|
|
15
15
|
__VITE_ASSETS__
|
|
16
|
-
|
|
17
|
-
<script>
|
|
18
|
-
tailwind.config = {
|
|
19
|
-
darkMode: 'class'
|
|
20
|
-
}
|
|
21
|
-
</script>
|
|
16
|
+
__STYLES_SKELETON__
|
|
22
17
|
</head>
|
|
23
18
|
|
|
24
19
|
<body class="__CLASS_BODY__">
|
|
25
|
-
<div id="root" data-react-component="__COMPONENT__" data-props='__PROPS__'
|
|
20
|
+
<div id="root" data-react-component="__COMPONENT__" data-props='__PROPS__'>
|
|
21
|
+
__SKELETON__
|
|
22
|
+
</div>
|
|
26
23
|
__SCRIPTS_BODY__
|
|
27
24
|
</body>
|
|
28
25
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { lazy, Suspense } from 'react';
|
|
2
2
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
|
3
|
-
import Home from './pages/Home';
|
|
4
|
-
import About from './pages/About';
|
|
5
3
|
import { ThemeProvider } from './blue-bird/contexts/ThemeContext.jsx';
|
|
4
|
+
import Skeleton from './blue-bird/components/Skeleton.jsx';
|
|
5
|
+
import { LanguageProvider } from './blue-bird/contexts/LanguageContext.jsx'
|
|
6
|
+
|
|
7
|
+
const Home = lazy(() => import('./pages/Home'));
|
|
8
|
+
const About = lazy(() => import('./pages/About'));
|
|
6
9
|
|
|
7
10
|
export default function App(_props) {
|
|
8
11
|
const {
|
|
@@ -16,12 +19,16 @@ export default function App(_props) {
|
|
|
16
19
|
|
|
17
20
|
return (
|
|
18
21
|
<ThemeProvider>
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
<LanguageProvider>
|
|
23
|
+
<Router>
|
|
24
|
+
<Suspense fallback={<Skeleton />}>
|
|
25
|
+
<Routes>
|
|
26
|
+
<Route path="/" element={<Home />} />
|
|
27
|
+
<Route path="/about" element={<About />} />
|
|
28
|
+
</Routes>
|
|
29
|
+
</Suspense>
|
|
30
|
+
</Router>
|
|
31
|
+
</LanguageProvider>
|
|
25
32
|
</ThemeProvider>
|
|
26
33
|
);
|
|
27
34
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
3
|
import App from './App';
|
|
4
|
+
import "../css/tailwind.css"
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
document.addEventListener('DOMContentLoaded', () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
document.querySelectorAll('[data-react-component]').forEach(el => {
|
|
9
|
+
const component = {
|
|
10
|
+
component: el.dataset.reactComponent
|
|
11
|
+
};
|
|
12
|
+
const props = JSON.parse(el.dataset.props || '{}');
|
|
13
|
+
const allProps = {
|
|
14
|
+
...props,
|
|
15
|
+
...component
|
|
16
|
+
}
|
|
17
|
+
createRoot(el).render(<App {...allProps} />);
|
|
18
|
+
});
|
|
18
19
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import Typography from './Typography';
|
|
2
3
|
|
|
3
4
|
export default function Card({ children, className = '', title, description, border = true, shadow = true }) {
|
|
4
5
|
return (
|
|
5
6
|
<div className={`rounded-lg ${border ? "border border-slate-200 dark:border-slate-800" : ""} ${shadow ? "shadow-sm" : ""} bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 ${className}`}>
|
|
6
7
|
{(title || description) && (
|
|
7
8
|
<div className="flex flex-col space-y-1.5 p-6">
|
|
8
|
-
{title && <
|
|
9
|
-
{description && <p className="text-sm text-slate-500 dark:text-slate-400">{description}</
|
|
9
|
+
{title && <Typography variant='h5' className="font-semibold leading-none tracking-tight">{title}</Typography>}
|
|
10
|
+
{description && <Typography variant='p' className="text-sm text-slate-500 dark:text-slate-400">{description}</Typography>}
|
|
10
11
|
</div>
|
|
11
12
|
)}
|
|
12
13
|
<div className={`p-6 ${title || description ? 'pt-0' : ''}`}>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export default function Skeleton() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="min-h-screen w-full bg-gray-50 p-4 md:p-8">
|
|
4
|
+
<div className="animate-pulse flex flex-col gap-6">
|
|
5
|
+
|
|
6
|
+
<div className="flex items-center justify-between w-full mb-4">
|
|
7
|
+
<div className="h-10 w-32 bg-gray-300 rounded-lg"></div>
|
|
8
|
+
<div className="flex space-x-4">
|
|
9
|
+
<div className="h-10 w-10 bg-gray-300 rounded-full"></div>
|
|
10
|
+
<div className="h-10 w-24 bg-gray-300 rounded-lg"></div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div className="h-48 md:h-64 w-full bg-gray-300 rounded-2xl"></div>
|
|
15
|
+
|
|
16
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
17
|
+
<div className="space-y-3">
|
|
18
|
+
<div className="h-40 w-full bg-gray-300 rounded-xl"></div>
|
|
19
|
+
<div className="h-4 w-3/4 bg-gray-300 rounded"></div>
|
|
20
|
+
<div className="h-4 w-1/2 bg-gray-300 rounded"></div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div className="space-y-3">
|
|
24
|
+
<div className="h-40 w-full bg-gray-300 rounded-xl"></div>
|
|
25
|
+
<div className="h-4 w-3/4 bg-gray-300 rounded"></div>
|
|
26
|
+
<div className="h-4 w-1/2 bg-gray-300 rounded"></div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div className="space-y-3">
|
|
30
|
+
<div className="h-40 w-full bg-gray-300 rounded-xl"></div>
|
|
31
|
+
<div className="h-4 w-3/4 bg-gray-300 rounded"></div>
|
|
32
|
+
<div className="h-4 w-1/2 bg-gray-300 rounded"></div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="space-y-2 mt-4">
|
|
37
|
+
<div className="h-4 w-full bg-gray-200 rounded"></div>
|
|
38
|
+
<div className="h-4 w-full bg-gray-200 rounded"></div>
|
|
39
|
+
<div className="h-4 w-2/3 bg-gray-200 rounded"></div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -17,7 +17,18 @@ export const LanguageProvider = ({ children }) => {
|
|
|
17
17
|
* @param {string} key - the string config key
|
|
18
18
|
* @returns {string} The translated text
|
|
19
19
|
*/
|
|
20
|
-
const t = (key) =>
|
|
20
|
+
const t = (key) => {
|
|
21
|
+
const keys = key.split('.');
|
|
22
|
+
let value = translations[lang];
|
|
23
|
+
for (const k of keys) {
|
|
24
|
+
if (value && typeof value === 'object' && value !== null && k in value) {
|
|
25
|
+
value = value[k];
|
|
26
|
+
} else {
|
|
27
|
+
return key;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return value !== undefined ? value : key;
|
|
31
|
+
};
|
|
21
32
|
|
|
22
33
|
return (
|
|
23
34
|
<LanguageContext.Provider value={{ lang, setLang, t }}>
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
+
"home_page": {
|
|
3
|
+
"title": "Welcome to Blue Bird",
|
|
4
|
+
"description": "The elegant, fast, and weightless framework for modern web development.",
|
|
5
|
+
"lightweight": "Lightweight",
|
|
6
|
+
"lightweightDescription": "Built with performance and simplicity in mind.",
|
|
7
|
+
"reactPowered": "React Powered",
|
|
8
|
+
"reactPoweredDescription": "Full React + Vite integration.",
|
|
9
|
+
"expressBackend": "Express Backend",
|
|
10
|
+
"expressBackendDescription": "Robust and scalable backend architecture."
|
|
11
|
+
},
|
|
12
|
+
"about_page": {
|
|
13
|
+
"title": "About Blue Bird",
|
|
14
|
+
"description": "Blue Bird is a modern framework designed to bridge the gap between backend routing and frontend interactivity.",
|
|
15
|
+
"description2": "It provides a seamless developer experience for building fast, reactive web applications.",
|
|
16
|
+
"check_your_console": "Check your console JS"
|
|
17
|
+
},
|
|
18
|
+
"home": "Home",
|
|
19
|
+
"about": "About",
|
|
2
20
|
"login": "Login",
|
|
3
21
|
"register": "Register",
|
|
4
22
|
"email": "Email address",
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
+
"home_page": {
|
|
3
|
+
"title": "Bienvenido a Blue Bird",
|
|
4
|
+
"description": "El framework elegante, rápido y ligero para el desarrollo web moderno.",
|
|
5
|
+
"lightweight": "Ligero",
|
|
6
|
+
"lightweightDescription": "Construido pensando en el rendimiento y la simplicidad.",
|
|
7
|
+
"reactPowered": "Impulsado por React",
|
|
8
|
+
"reactPoweredDescription": "Integración completa con React + Vite.",
|
|
9
|
+
"expressBackend": "Backend con Express",
|
|
10
|
+
"expressBackendDescription": "Arquitectura de backend robusta y escalable."
|
|
11
|
+
},
|
|
12
|
+
"about_page": {
|
|
13
|
+
"title": "Acerca de Blue Bird",
|
|
14
|
+
"description": "Blue Bird es un framework moderno diseñado para cerrar la brecha entre el enrutamiento de backend y la interactividad de frontend.",
|
|
15
|
+
"description2": "Proporciona una experiencia de desarrollador perfecta para crear aplicaciones web rápidas y reactivas.",
|
|
16
|
+
"check_your_console": "Revisa tu consola JS"
|
|
17
|
+
},
|
|
18
|
+
"home": "Inicio",
|
|
19
|
+
"about": "Acerca de",
|
|
2
20
|
"login": "Iniciar Sesión",
|
|
3
21
|
"register": "Registrarse",
|
|
4
22
|
"email": "Correo Electrónico",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import Button from "../blue-bird/components/Button";
|
|
4
|
+
import { useLanguage } from "../blue-bird/contexts/LanguageContext";
|
|
5
|
+
import { useTheme } from "../blue-bird/contexts/ThemeContext";
|
|
6
|
+
|
|
7
|
+
export default function Header() {
|
|
8
|
+
const { t, setLang } = useLanguage();
|
|
9
|
+
const { changeTheme } = useTheme();
|
|
10
|
+
|
|
11
|
+
const [emojiTheme, setEmojiTheme] = useState("🌞");
|
|
12
|
+
|
|
13
|
+
const changeThemeEmoji = () => {
|
|
14
|
+
if (emojiTheme === "🌞") {
|
|
15
|
+
setEmojiTheme("🌙");
|
|
16
|
+
changeTheme("dark");
|
|
17
|
+
} else {
|
|
18
|
+
setEmojiTheme("🌞");
|
|
19
|
+
changeTheme("light");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<header>
|
|
25
|
+
<nav
|
|
26
|
+
className='bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-slate-800 px-4 py-4 flex justify-between items-center gap-4 sticky top-0 z-10'
|
|
27
|
+
>
|
|
28
|
+
<div className='font-bold text-xl text-slate-900 dark:text-slate-100'>
|
|
29
|
+
Blue Bird
|
|
30
|
+
</div>
|
|
31
|
+
<div className='flex justify-between items-center gap-4'>
|
|
32
|
+
<div className="flex justify-between items-center gap-4">
|
|
33
|
+
<Button variant="outline" size="sm" onClick={() => setLang("es")} >
|
|
34
|
+
ES
|
|
35
|
+
</Button>
|
|
36
|
+
<Button variant="outline" size="sm" onClick={() => setLang("en")} >
|
|
37
|
+
EN
|
|
38
|
+
</Button>
|
|
39
|
+
<Button variant="ghost" size="icon" onClick={changeThemeEmoji}>
|
|
40
|
+
{emojiTheme}
|
|
41
|
+
</Button>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="flex justify-between items-center gap-4">
|
|
44
|
+
<Link to="/" className='text-sm font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 transition-colors'>{t("home")}</Link>
|
|
45
|
+
<Link to="/about" className='text-sm font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 transition-colors'>{t("about")}</Link>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
</nav>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -1,33 +1,31 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import Header from '../components/Header';
|
|
3
|
+
import { useLanguage } from '../blue-bird/contexts/LanguageContext';
|
|
4
|
+
|
|
5
|
+
import Card from '../blue-bird/components/Card';
|
|
6
|
+
import Typography from '../blue-bird/components/Typography'
|
|
3
7
|
|
|
4
8
|
export default function About() {
|
|
9
|
+
const { t } = useLanguage();
|
|
5
10
|
return (
|
|
6
11
|
<div
|
|
7
12
|
className="bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 min-h-screen"
|
|
8
13
|
>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Blue Bird is a modern framework designed to bridge the gap between backend routing and frontend interactivity.
|
|
25
|
-
It provides a seamless developer experience for building fast, reactive web applications.
|
|
26
|
-
</p>
|
|
27
|
-
<p className='text-red-500 text-xl mt-8 '>
|
|
28
|
-
Check your console JS
|
|
29
|
-
</p>
|
|
30
|
-
</div>
|
|
14
|
+
<Header />
|
|
15
|
+
<main className='max-w-3xl mx-auto mt-8 p-4'>
|
|
16
|
+
<Card>
|
|
17
|
+
<Typography variant='h1' className='text-3xl font-bold tracking-tight text-slate-900 dark:text-slate-100 mb-4'>
|
|
18
|
+
{t("about_page.title")}
|
|
19
|
+
</Typography>
|
|
20
|
+
<Typography className='text-slate-500 dark:text-slate-400 leading-7'>
|
|
21
|
+
{t("about_page.description")}
|
|
22
|
+
</Typography>
|
|
23
|
+
<div className='mt-8 pt-4 border-t border-slate-200 dark:border-slate-800'>
|
|
24
|
+
<Typography className='text-sm text-red-500 font-medium'>
|
|
25
|
+
{t("about_page.check_your_console")}
|
|
26
|
+
</Typography>
|
|
27
|
+
</div>
|
|
28
|
+
</Card>
|
|
31
29
|
</main>
|
|
32
30
|
</div>
|
|
33
31
|
);
|