@seip/blue-bird 0.3.1 → 0.3.4

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 (43) hide show
  1. package/.env_example +23 -13
  2. package/LICENSE +21 -21
  3. package/README.md +79 -79
  4. package/backend/index.js +12 -12
  5. package/backend/routes/api.js +34 -34
  6. package/backend/routes/frontend.js +1 -8
  7. package/core/app.js +359 -359
  8. package/core/auth.js +69 -69
  9. package/core/cache.js +35 -35
  10. package/core/cli/component.js +42 -42
  11. package/core/cli/init.js +120 -118
  12. package/core/cli/react.js +383 -409
  13. package/core/cli/route.js +42 -42
  14. package/core/cli/scaffolding-auth.js +967 -0
  15. package/core/config.js +41 -41
  16. package/core/debug.js +248 -248
  17. package/core/logger.js +80 -80
  18. package/core/middleware.js +27 -27
  19. package/core/router.js +134 -134
  20. package/core/swagger.js +24 -24
  21. package/core/template.js +288 -288
  22. package/core/upload.js +76 -76
  23. package/core/validate.js +291 -290
  24. package/frontend/index.html +28 -22
  25. package/frontend/resources/js/App.jsx +28 -42
  26. package/frontend/resources/js/Main.jsx +17 -17
  27. package/frontend/resources/js/blue-bird/components/Button.jsx +67 -0
  28. package/frontend/resources/js/blue-bird/components/Card.jsx +17 -0
  29. package/frontend/resources/js/blue-bird/components/DataTable.jsx +126 -0
  30. package/frontend/resources/js/blue-bird/components/Input.jsx +21 -0
  31. package/frontend/resources/js/blue-bird/components/Label.jsx +12 -0
  32. package/frontend/resources/js/blue-bird/components/Modal.jsx +27 -0
  33. package/frontend/resources/js/blue-bird/components/Translate.jsx +12 -0
  34. package/frontend/resources/js/blue-bird/components/Typography.jsx +25 -0
  35. package/frontend/resources/js/blue-bird/contexts/LanguageContext.jsx +29 -0
  36. package/frontend/resources/js/blue-bird/contexts/SnackbarContext.jsx +38 -0
  37. package/frontend/resources/js/blue-bird/contexts/ThemeContext.jsx +49 -0
  38. package/frontend/resources/js/blue-bird/locales/en.json +30 -0
  39. package/frontend/resources/js/blue-bird/locales/es.json +30 -0
  40. package/frontend/resources/js/pages/About.jsx +33 -15
  41. package/frontend/resources/js/pages/Home.jsx +93 -68
  42. package/package.json +56 -55
  43. package/vite.config.js +21 -21
@@ -1,42 +1,28 @@
1
- import React from 'react';
2
- import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
3
- import Home from './pages/Home';
4
- import About from './pages/About';
5
-
6
- export default function App(_props) {
7
- const {
8
- component,
9
- props
10
- } = _props;
11
-
12
- console.log(`Check props and component `)
13
- console.log(`Component: ${component}`)
14
- console.log(props)
15
-
16
- return (
17
- <Router>
18
- <div
19
- className="bg-white text-gray-900"
20
- >
21
- <nav
22
- className='bg-white text-gray-900 border border-gray-200 px-4 py-4 flex justify-between items-center gap-4 sticky top-0 z-10'
23
- >
24
- <div className='font-bold text-xl text-blue-600'>
25
- Blue Bird
26
- </div>
27
- <div className='flex justify-between items-center gap-4'>
28
- <Link to="/" className='text-gray-500 hover:text-gray-900'>Home</Link>
29
- <Link to="/about" className='text-gray-500 hover:text-gray-900'>About</Link>
30
- </div>
31
- </nav>
32
- <main className='max-w-7xl mx-auto'>
33
- <Routes>
34
- <Route path="/" element={<Home />} />
35
- <Route path="/about" element={<About />} />
36
- </Routes>
37
- </main>
38
- </div>
39
- </Router>
40
- );
41
- }
42
-
1
+ import React from 'react';
2
+ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
3
+ import Home from './pages/Home';
4
+ import About from './pages/About';
5
+ import { ThemeProvider } from './blue-bird/contexts/ThemeContext.jsx';
6
+
7
+ export default function App(_props) {
8
+ const {
9
+ component,
10
+ props
11
+ } = _props;
12
+
13
+ console.log(`Check props and component `)
14
+ console.log(`Component: ${component}`)
15
+ console.log(props)
16
+
17
+ return (
18
+ <ThemeProvider>
19
+ <Router>
20
+ <Routes>
21
+ <Route path="/" element={<Home />} />
22
+ <Route path="/about" element={<About />} />
23
+ </Routes>
24
+ </Router>
25
+ </ThemeProvider>
26
+ );
27
+ }
28
+
@@ -1,18 +1,18 @@
1
- import React from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import App from './App';
4
-
5
-
6
- document.addEventListener('DOMContentLoaded', () => {
7
- document.querySelectorAll('[data-react-component]').forEach(el => {
8
- const component = {
9
- component:el.dataset.reactComponent
10
- };
11
- const props = JSON.parse(el.dataset.props || '{}');
12
- const allProps={
13
- ...props,
14
- ...component
15
- }
16
- createRoot(el).render(<App {...allProps} />);
17
- });
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App';
4
+
5
+
6
+ document.addEventListener('DOMContentLoaded', () => {
7
+ document.querySelectorAll('[data-react-component]').forEach(el => {
8
+ const component = {
9
+ component:el.dataset.reactComponent
10
+ };
11
+ const props = JSON.parse(el.dataset.props || '{}');
12
+ const allProps={
13
+ ...props,
14
+ ...component
15
+ }
16
+ createRoot(el).render(<App {...allProps} />);
17
+ });
18
18
  });
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+
3
+ export default function Button({ children, variant = 'default', size = 'default', className = '', ...props }) {
4
+ const baseStyle = "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
5
+
6
+ const variants = {
7
+ default: "bg-slate-900 dark:bg-slate-100 text-slate-50 dark:text-slate-900 hover:bg-slate-900/90 dark:hover:bg-slate-100/90",
8
+ destructive: "bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:hover:bg-red-900/90",
9
+ outline: "border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800",
10
+ secondary: "bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 hover:bg-slate-100/80 dark:hover:bg-slate-800/80",
11
+ ghost: "hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-900 dark:text-slate-100",
12
+ link: "text-slate-900 dark:text-slate-100 underline-offset-4 hover:underline",
13
+ fill: "bg-gray-100 dark:bg-slate-800 text-gray-900 dark:text-slate-100 hover:bg-gray-100/80 dark:hover:bg-slate-800/80 w-full",
14
+ blue: "bg-blue-500 text-white hover:bg-blue-500/90 w-full",
15
+ blue_light: "bg-blue-100 text-blue-500 hover:bg-blue-100/80 w-full font-semibold",
16
+ green: "bg-green-500 text-white hover:bg-green-500/90 w-full",
17
+ green_light: "bg-green-100 text-green-500 hover:bg-green-100/80 w-full font-semibold",
18
+ red: "bg-red-500 text-white hover:bg-red-500/90 w-full",
19
+ red_light: "bg-red-100 text-red-500 hover:bg-red-100/80 w-full font-semibold",
20
+ yellow: "bg-yellow-500 text-white hover:bg-yellow-500/90 w-full",
21
+ yellow_light: "bg-yellow-100 text-yellow-500 hover:bg-yellow-100/80 w-full font-semibold",
22
+ purple: "bg-purple-500 text-white hover:bg-purple-500/90 w-full",
23
+ purple_light: "bg-purple-100 text-purple-500 hover:bg-purple-100/80 w-full font-semibold",
24
+ pink: "bg-pink-500 text-white hover:bg-pink-500/90 w-full",
25
+ pink_light: "bg-pink-100 text-pink-500 hover:bg-pink-100/80 w-full font-semibold",
26
+ orange: "bg-orange-500 text-white hover:bg-orange-500/90 w-full",
27
+ orange_light: "bg-orange-100 text-orange-500 hover:bg-orange-100/80 w-full font-semibold",
28
+ cyan: "bg-cyan-500 text-white hover:bg-cyan-500/90 w-full",
29
+ cyan_light: "bg-cyan-100 text-cyan-500 hover:bg-cyan-100/80 w-full font-semibold",
30
+ teal: "bg-teal-500 text-white hover:bg-teal-500/90 w-full",
31
+ teal_light: "bg-teal-100 text-teal-500 hover:bg-teal-100/80 w-full font-semibold",
32
+ lime: "bg-lime-500 text-white hover:bg-lime-500/90 w-full",
33
+ lime_light: "bg-lime-100 text-lime-500 hover:bg-lime-100/80 w-full font-semibold",
34
+ indigo: "bg-indigo-500 text-white hover:bg-indigo-500/90 w-full",
35
+ indigo_light: "bg-indigo-100 text-indigo-500 hover:bg-indigo-100/80 w-full font-semibold",
36
+ violet: "bg-violet-500 text-white hover:bg-violet-500/90 w-full",
37
+ violet_light: "bg-violet-100 text-violet-500 hover:bg-violet-100/80 w-full font-semibold",
38
+ fuchsia: "bg-fuchsia-500 text-white hover:bg-fuchsia-500/90 w-full",
39
+ fuchsia_light: "bg-fuchsia-100 text-fuchsia-500 hover:bg-fuchsia-100/80 w-full font-semibold",
40
+ rose: "bg-rose-500 text-white hover:bg-rose-500/90 w-full",
41
+ rose_light: "bg-rose-100 text-rose-500 hover:bg-rose-100/80 w-full font-semibold",
42
+ emerald: "bg-emerald-500 text-white hover:bg-emerald-500/90 w-full",
43
+ emerald_light: "bg-emerald-100 text-emerald-500 hover:bg-emerald-100/80 w-full font-semibold",
44
+ sky: "bg-sky-500 text-white hover:bg-sky-500/90 w-full",
45
+ sky_light: "bg-sky-100 text-sky-500 hover:bg-sky-100/80 w-full font-semibold",
46
+ slate: "bg-slate-500 text-white hover:bg-slate-500/90 w-full",
47
+ gray: "bg-gray-500 text-white hover:bg-gray-500/90 w-full",
48
+ zinc: "bg-zinc-500 text-white hover:bg-zinc-500/90 w-full",
49
+ neutral: "bg-neutral-500 text-white hover:bg-neutral-500/90 w-full",
50
+ stone: "bg-stone-500 text-white hover:bg-stone-500/90 w-full",
51
+ };
52
+
53
+ const sizes = {
54
+ default: "h-10 px-4 py-2",
55
+ sm: "h-9 rounded-md px-3",
56
+ lg: "h-11 rounded-md px-8",
57
+ icon: "h-10 w-10"
58
+ };
59
+
60
+ const style = `${baseStyle} ${variants[variant] || variants.default} ${sizes[size] || sizes.default} ${className}`;
61
+
62
+ return (
63
+ <button className={style} {...props}>
64
+ {children}
65
+ </button>
66
+ );
67
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+
3
+ export default function Card({ children, className = '', title, description, border = true, shadow = true }) {
4
+ return (
5
+ <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
+ {(title || description) && (
7
+ <div className="flex flex-col space-y-1.5 p-6">
8
+ {title && <h3 className="font-semibold leading-none tracking-tight">{title}</h3>}
9
+ {description && <p className="text-sm text-slate-500 dark:text-slate-400">{description}</p>}
10
+ </div>
11
+ )}
12
+ <div className={`p-6 ${title || description ? 'pt-0' : ''}`}>
13
+ {children}
14
+ </div>
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,126 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import { useLanguage } from '../contexts/LanguageContext.jsx';
3
+ import Button from './Button.jsx';
4
+ import Input from './Input.jsx';
5
+
6
+ export default function DataTable({
7
+ data = [],
8
+ columns = [],
9
+ rowsPerPage = 10,
10
+ searchable = true,
11
+ pagination = true,
12
+ onEdit,
13
+ onDelete
14
+ }) {
15
+ const { t } = useLanguage();
16
+ const [currentPage, setCurrentPage] = useState(1);
17
+ const [searchTerm, setSearchTerm] = useState('');
18
+
19
+ const filteredData = useMemo(() => {
20
+ if (!searchTerm) return data;
21
+ const lowerTerm = searchTerm.toLowerCase();
22
+ return data.filter(item =>
23
+ columns.some(col => String(item[col.key] || '').toLowerCase().includes(lowerTerm))
24
+ );
25
+ }, [data, columns, searchTerm]);
26
+
27
+ const pageCount = Math.ceil(filteredData.length / rowsPerPage);
28
+ const paginatedData = useMemo(() => {
29
+ if (!pagination) return filteredData;
30
+ const start = (currentPage - 1) * rowsPerPage;
31
+ return filteredData.slice(start, start + rowsPerPage);
32
+ }, [filteredData, currentPage, rowsPerPage, pagination]);
33
+
34
+ return (
35
+ <div className="flex flex-col w-full gap-4">
36
+ {searchable && (
37
+ <div className="flex justify-end">
38
+ <Input
39
+ placeholder={t('search')}
40
+ value={searchTerm}
41
+ onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }}
42
+ className="w-full md:w-64"
43
+ />
44
+ </div>
45
+ )}
46
+
47
+ <div className="overflow-x-auto bg-white dark:bg-slate-900 rounded-lg shadow border border-gray-200 dark:border-slate-800">
48
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-slate-800">
49
+ <thead className="bg-gray-50 dark:bg-slate-800/50">
50
+ <tr>
51
+ {columns.map(col => (
52
+ <th key={col.key} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
53
+ {col.title || col.key}
54
+ </th>
55
+ ))}
56
+ {(onEdit || onDelete) && (
57
+ <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
58
+ {t('actions')}
59
+ </th>
60
+ )}
61
+ </tr>
62
+ </thead>
63
+ <tbody className="bg-white dark:bg-slate-900 divide-y divide-gray-200 dark:divide-slate-800">
64
+ {paginatedData.length > 0 ? paginatedData.map((row, idx) => (
65
+ <tr key={row.id || idx} className="hover:bg-gray-50 dark:hover:bg-slate-800/50 transition-colors">
66
+ {columns.map(col => (
67
+ <td key={col.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
68
+ {row[col.key] || '-'}
69
+ </td>
70
+ ))}
71
+ {(onEdit || onDelete) && (
72
+ <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
73
+ {onEdit && (
74
+ <Button variant="outline" className="px-2 py-1 text-xs" onClick={() => onEdit(row)}>
75
+ {t('edit')}
76
+ </Button>
77
+ )}
78
+ {onDelete && (
79
+ <Button variant="danger" className="px-2 py-1 text-xs" onClick={() => onDelete(row)}>
80
+ {t('delete')}
81
+ </Button>
82
+ )}
83
+ </td>
84
+ )}
85
+ </tr>
86
+ )) : (
87
+ <tr>
88
+ <td colSpan={columns.length + (onEdit || onDelete ? 1 : 0)} className="px-6 py-4 text-center text-sm text-gray-500 dark:text-slate-400">
89
+ No data available.
90
+ </td>
91
+ </tr>
92
+ )}
93
+ </tbody>
94
+ </table>
95
+ </div>
96
+
97
+ {pagination && pageCount > 1 && (
98
+ <div className="flex justify-end mt-4 gap-1">
99
+ <Button
100
+ variant="secondary"
101
+ disabled={currentPage === 1}
102
+ onClick={() => setCurrentPage(p => p - 1)}
103
+ >
104
+ &laquo;
105
+ </Button>
106
+ {Array.from({ length: pageCount }).map((_, i) => (
107
+ <Button
108
+ key={i}
109
+ variant={currentPage === i + 1 ? 'primary' : 'ghost'}
110
+ onClick={() => setCurrentPage(i + 1)}
111
+ >
112
+ {i + 1}
113
+ </Button>
114
+ ))}
115
+ <Button
116
+ variant="secondary"
117
+ disabled={currentPage === pageCount}
118
+ onClick={() => setCurrentPage(p => p + 1)}
119
+ >
120
+ &raquo;
121
+ </Button>
122
+ </div>
123
+ )}
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import Label from './Label.jsx';
3
+
4
+ export default function Input({ label, error, variant = "default", className = '', ...props }) {
5
+ const variants = {
6
+ default: "flex h-10 w-full rounded-md border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 px-3 py-2 text-sm ring-offset-white dark:ring-offset-slate-950 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 dark:placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 dark:focus-visible:ring-slate-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
7
+ error: "flex h-10 w-full rounded-md border border-red-500 dark:border-red-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 px-3 py-2 text-sm ring-offset-white dark:ring-offset-slate-950 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 dark:placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
8
+ fill: "flex h-10 w-full rounded-md border border-gray-100 dark:border-slate-800 bg-gray-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 px-3 py-2 text-sm ring-offset-white dark:ring-offset-slate-950 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 dark:placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 dark:focus-visible:ring-slate-300 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 w-full"
9
+ }
10
+ const style = variants[variant] || variants.default;
11
+ return (
12
+ <div className={`flex flex-col gap-1.5 ${className}`}>
13
+ {label && <Label>{label}</Label>}
14
+ <input
15
+ className={`${style} ${error ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
16
+ {...props}
17
+ />
18
+ {error && <span className="text-xs font-medium text-red-500">{error}</span>}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ export default function Label({ children, className = '', ...props }) {
4
+ return (
5
+ <label
6
+ className={`text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${className}`}
7
+ {...props}
8
+ >
9
+ {children}
10
+ </label>
11
+ );
12
+ }
@@ -0,0 +1,27 @@
1
+ import React, { useEffect } from 'react';
2
+
3
+ export default function Modal({ isOpen, onClose, title, children }) {
4
+ useEffect(() => {
5
+ if (isOpen) document.body.style.overflow = 'hidden';
6
+ else document.body.style.overflow = 'unset';
7
+ return () => { document.body.style.overflow = 'unset'; };
8
+ }, [isOpen]);
9
+
10
+ if (!isOpen) return null;
11
+
12
+ return (
13
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 transition-opacity">
14
+ <div className="bg-white dark:bg-slate-900 rounded-lg shadow-xl w-full max-w-lg mx-4 overflow-hidden transform transition-all border dark:border-slate-800">
15
+ <div className="flex justify-between items-center p-4 border-b dark:border-slate-800">
16
+ <h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{title}</h3>
17
+ <button onClick={onClose} className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-2xl leading-none">
18
+ &times;
19
+ </button>
20
+ </div>
21
+ <div className="p-4 text-slate-900 dark:text-slate-100">
22
+ {children}
23
+ </div>
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { useLanguage } from '../contexts/LanguageContext.jsx';
3
+
4
+ /**
5
+ * Renders translated text string given a key.
6
+ * @param {Object} props
7
+ * @param {string} props.k - The translation key.
8
+ */
9
+ export default function Translate({ k }) {
10
+ const { t } = useLanguage();
11
+ return <>{t(k)}</>;
12
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+
3
+ export default function Typography({ variant = 'p', children, className = '', ...props }) {
4
+ const variants = {
5
+ h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
6
+ h2: "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
7
+ h3: "scroll-m-20 text-2xl font-semibold tracking-tight",
8
+ h4: "scroll-m-20 text-xl font-semibold tracking-tight",
9
+ p: "leading-7 [&:not(:first-child)]:mt-6",
10
+ blockquote: "mt-6 border-l-2 pl-6 italic",
11
+ lead: "text-xl text-slate-700 dark:text-slate-300",
12
+ large: "text-lg font-semibold",
13
+ small: "text-sm font-medium leading-none",
14
+ muted: "text-sm text-slate-500 dark:text-slate-400",
15
+ };
16
+
17
+ const Component = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p'].includes(variant) ? variant : 'p';
18
+ const style = `${variants[variant]} ${className}`;
19
+
20
+ return (
21
+ <Component className={style} {...props}>
22
+ {children}
23
+ </Component>
24
+ );
25
+ }
@@ -0,0 +1,29 @@
1
+ import React, { createContext, useState, useEffect, useContext } from 'react';
2
+ import en from '../locales/en.json';
3
+ import es from '../locales/es.json';
4
+
5
+ const translations = { en, es };
6
+ export const LanguageContext = createContext();
7
+
8
+ export const LanguageProvider = ({ children }) => {
9
+ const [lang, setLang] = useState(() => localStorage.getItem('lila_lang') || 'en');
10
+
11
+ useEffect(() => {
12
+ localStorage.setItem('lila_lang', lang);
13
+ }, [lang]);
14
+
15
+ /**
16
+ * Translate key into configured language text
17
+ * @param {string} key - the string config key
18
+ * @returns {string} The translated text
19
+ */
20
+ const t = (key) => translations[lang][key] || key;
21
+
22
+ return (
23
+ <LanguageContext.Provider value={{ lang, setLang, t }}>
24
+ {children}
25
+ </LanguageContext.Provider>
26
+ );
27
+ };
28
+
29
+ export const useLanguage = () => useContext(LanguageContext);
@@ -0,0 +1,38 @@
1
+ import React, { createContext, useState, useContext, useCallback } from 'react';
2
+
3
+ export const SnackbarContext = createContext();
4
+
5
+ export const SnackbarProvider = ({ children }) => {
6
+ const [snackbars, setSnackbars] = useState([]);
7
+
8
+ const showSnackbar = useCallback((message, type = 'info', duration = 3000) => {
9
+ const id = Date.now();
10
+ setSnackbars((prev) => [...prev, { id, message, type }]);
11
+ setTimeout(() => {
12
+ setSnackbars((prev) => prev.filter((s) => s.id !== id));
13
+ }, duration);
14
+ }, []);
15
+
16
+ return (
17
+ <SnackbarContext.Provider value={{ showSnackbar }}>
18
+ {children}
19
+ <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
20
+ {snackbars.map((s) => (
21
+ <div
22
+ key={s.id}
23
+ className={`px-4 py-3 rounded shadow-lg text-white transition-all transform pointer-events-auto ${s.type === 'success' ? 'bg-green-600' :
24
+ s.type === 'error' ? 'bg-red-600' :
25
+ s.type === 'warning' ? 'bg-yellow-600' :
26
+ 'bg-blue-600'
27
+ }`}
28
+ role="alert"
29
+ >
30
+ {s.message}
31
+ </div>
32
+ ))}
33
+ </div>
34
+ </SnackbarContext.Provider>
35
+ );
36
+ };
37
+
38
+ export const useSnackbar = () => useContext(SnackbarContext);
@@ -0,0 +1,49 @@
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+
3
+ const ThemeContext = createContext();
4
+
5
+ export function ThemeProvider({ children }) {
6
+ const [theme, setTheme] = useState(() => {
7
+ return localStorage.getItem('theme') || 'system';
8
+ });
9
+
10
+ useEffect(() => {
11
+ const root = window.document.documentElement;
12
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
13
+
14
+ const applyTheme = (currentTheme) => {
15
+ root.classList.remove('light', 'dark');
16
+
17
+ if (currentTheme === 'system') {
18
+ const systemTheme = mediaQuery.matches ? 'dark' : 'light';
19
+ root.classList.add(systemTheme);
20
+ } else {
21
+ root.classList.add(currentTheme);
22
+ }
23
+ };
24
+
25
+ applyTheme(theme);
26
+
27
+ const handleChange = () => {
28
+ if (theme === 'system') {
29
+ applyTheme('system');
30
+ }
31
+ };
32
+
33
+ mediaQuery.addEventListener('change', handleChange);
34
+ return () => mediaQuery.removeEventListener('change', handleChange);
35
+ }, [theme]);
36
+
37
+ const changeTheme = (newTheme) => {
38
+ setTheme(newTheme);
39
+ localStorage.setItem('theme', newTheme);
40
+ };
41
+
42
+ return (
43
+ <ThemeContext.Provider value={{ theme, changeTheme }}>
44
+ {children}
45
+ </ThemeContext.Provider>
46
+ );
47
+ }
48
+
49
+ export const useTheme = () => useContext(ThemeContext);
@@ -0,0 +1,30 @@
1
+ {
2
+ "login": "Login",
3
+ "register": "Register",
4
+ "email": "Email address",
5
+ "password": "Password",
6
+ "name": "Full Name",
7
+ "submit": "Submit",
8
+ "forgot_password": "Forgot Password?",
9
+ "dashboard": "Dashboard",
10
+ "profile": "Profile",
11
+ "logout": "Logout",
12
+ "language": "Language",
13
+ "success_login": "Logged in successfully!",
14
+ "error_login": "Invalid credentials",
15
+ "error_general": "An error occurred. Please try again later.",
16
+ "search": "Search...",
17
+ "actions": "Actions",
18
+ "edit": "Edit",
19
+ "delete": "Delete",
20
+ "error_email_register": "Error, check your email entered",
21
+ "error_is_active_register": "Error, your account is not active",
22
+ "forgot_password_desc": "Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one",
23
+ "password_confirmation": "Confirm Password",
24
+ "back_to_login": "Back to login",
25
+ "dont_have_account_register": "Don't have an account? Register",
26
+ "password_confirmation_err": "Error, the password must match",
27
+ "deleted_account_error": "Error, your account has been deleted",
28
+ "error_token_reset": "Invalid or expired reset token.",
29
+ "If the email is valid, a password reset link has been sent": "If the email is valid, a password reset link has been sent."
30
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "login": "Iniciar Sesión",
3
+ "register": "Registrarse",
4
+ "email": "Correo Electrónico",
5
+ "password": "Contraseña",
6
+ "name": "Nombre Completo",
7
+ "submit": "Enviar",
8
+ "forgot_password": "¿Olvidó su contraseña?",
9
+ "dashboard": "Panel de Inicio",
10
+ "profile": "Perfil",
11
+ "logout": "Cerrar Sesión",
12
+ "language": "Idioma",
13
+ "success_login": "¡Inicio de sesión exitoso!",
14
+ "error_login": "Credenciales inválidas",
15
+ "error_general": "Ocurrió un error. Inténtelo más tarde.",
16
+ "search": "Buscar...",
17
+ "actions": "Acciones",
18
+ "edit": "Editar",
19
+ "delete": "Eliminar",
20
+ "error_email_register": "Error, revisa el email ingresado",
21
+ "error_is_active_register": "Error, tu cuenta no esta activa",
22
+ "forgot_password_desc": "¿Olvidó su contraseña? No hay problema. Simplemente indíquenos su dirección de correo electrónico y le enviaremos un enlace de restablecimiento de contraseña que le permitirá elegir una nueva",
23
+ "password_confirmation": "Confirmar Contraseña",
24
+ "back_to_login": "Volver a iniciar sesión",
25
+ "dont_have_account_register": "¿No tienes una cuenta? Regístrate",
26
+ "password_confirmation_err": "Error, las contraseñas deben de coincidir",
27
+ "deleted_account_error": "Error, tu cuenta ha sido eliminada",
28
+ "error_token_reset": "Token de restablecimiento inválido o expirado.",
29
+ "If the email is valid, a password reset link has been sent": "Si el correo electrónico es válido, se ha enviado un enlace para restablecer la contraseña."
30
+ }