@axzydev/axzy_ui_system 1.4.8 → 1.5.1
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/README.md +2 -11
- package/dist/index.cjs +1826 -1351
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +115 -14
- package/dist/index.d.ts +115 -14
- package/dist/index.js +1481 -1010
- package/dist/index.js.map +1 -1
- package/dist/layered.css +1 -1
- package/package.json +3 -16
- package/src/App.tsx +90 -73
- package/src/components/atoms/badget/badget.tsx +23 -5
- package/src/components/atoms/chip/chip.tsx +3 -1
- package/src/components/atoms/image/image.tsx +3 -1
- package/src/components/atoms/input/input.tsx +8 -5
- package/src/components/atoms/loader/loader.tsx +3 -1
- package/src/components/atoms/slide/slide.props.ts +12 -0
- package/src/components/atoms/slide/slide.tsx +3 -0
- package/src/components/molecules/alert/alert.tsx +3 -1
- package/src/components/molecules/breadcrumbs/breadcrumbs.tsx +3 -1
- package/src/components/molecules/calendar/calendar.tsx +9 -6
- package/src/components/molecules/card/card.tsx +5 -2
- package/src/components/molecules/chip-input/chip-input.tsx +9 -7
- package/src/components/molecules/date-picker/datePicker.tsx +9 -9
- package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +1 -1
- package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +6 -3
- package/src/components/molecules/field/field.tsx +3 -1
- package/src/components/molecules/form-header/form-header.tsx +3 -1
- package/src/components/molecules/line-bar-chart/line-bar-chart.props.ts +2 -2
- package/src/components/molecules/line-bar-chart/line-bar-chart.tsx +20 -15
- package/src/components/molecules/masked-input/masked-input.tsx +3 -1
- package/src/components/molecules/multi-select/multi-select.tsx +12 -9
- package/src/components/molecules/pagination/pagination.tsx +11 -5
- package/src/components/molecules/search-select/search-select.tsx +11 -9
- package/src/components/molecules/select/select.tsx +6 -4
- package/src/components/molecules/stepper/stepper.tsx +6 -4
- package/src/components/molecules/table/table.tsx +30 -28
- package/src/components/molecules/time-picker/timePicker.tsx +8 -6
- package/src/components/molecules/wysiwyg/wysiwyg.tsx +23 -12
- package/src/components/organisms/confirm-dialog/confirm-dialog.props.ts +4 -4
- package/src/components/organisms/confirm-dialog/confirm-dialog.tsx +16 -8
- package/src/components/organisms/data-table/dataTable.tsx +31 -25
- package/src/components/organisms/dialog/dialog.tsx +3 -0
- package/src/components/organisms/drawer/drawer.tsx +3 -0
- package/src/components/organisms/dropfile/dropfile.tsx +19 -17
- package/src/components/organisms/form-builder/fieldRenderer.tsx +4 -2
- package/src/components/organisms/navbar/navbar.tsx +4 -2
- package/src/components/organisms/page-header/page-header.tsx +3 -1
- package/src/components/organisms/searchTable/components/PaginationControls.tsx +8 -6
- package/src/components/organisms/searchTable/components/PaginationInfo.tsx +3 -1
- package/src/components/organisms/searchTable/components/SearchAndSortBar.tsx +7 -5
- package/src/components/organisms/searchTable/components/SearchInput.tsx +4 -2
- package/src/components/organisms/searchTable/components/SortButton.tsx +13 -11
- package/src/components/organisms/searchTable/components/TableEmptyState.tsx +4 -2
- package/src/components/organisms/searchTable/components/TableRow.tsx +6 -4
- package/src/components/organisms/searchTable/searchTable.tsx +4 -2
- package/src/components/organisms/sidebar/sidebar.props.ts +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +9 -4
- package/src/components/organisms/toast/toast.tsx +3 -1
- package/src/components/organisms/topbar/topbar.tsx +4 -2
- package/src/components/templates/page/page.props.ts +4 -4
- package/src/components/templates/page/page.tsx +6 -4
- package/src/components/theme-provider/themeProvider.tsx +28 -17
- package/src/dev.css +61 -2
- package/src/global.d.ts +10 -0
- package/src/i18n/context.ts +9 -0
- package/src/i18n/index.ts +22 -0
- package/src/i18n/locales/en.ts +163 -0
- package/src/i18n/locales/es.ts +165 -0
- package/src/i18n/provider.tsx +44 -0
- package/src/i18n/translator.ts +82 -0
- package/src/i18n/types.ts +54 -0
- package/src/i18n/useITTranslation.ts +20 -0
- package/src/index.css +0 -1
- package/src/index.ts +7 -1
- package/src/sandbox/LandingShowcase.tsx +224 -580
- package/src/sandbox/NotFoundShowcase.tsx +51 -36
- package/src/sandbox/TopbarSearch.tsx +6 -4
- package/src/sandbox/controls.tsx +180 -0
- package/src/sandbox/i18n.ts +1939 -0
- package/src/sandbox/icons.tsx +25 -0
- package/src/sandbox/locale.tsx +72 -0
- package/src/sandbox/ui.tsx +474 -0
- package/src/sandbox/useHashRoute.ts +46 -10
- package/src/showcases/DataShowcases.tsx +103 -87
- package/src/showcases/FeedbackShowcases.tsx +37 -30
- package/src/showcases/FormShowcases.tsx +303 -215
- package/src/showcases/GettingStartedShowcase.tsx +151 -145
- package/src/showcases/HelpShowcase.tsx +126 -128
- package/src/showcases/HomeShowcase.tsx +678 -482
- package/src/showcases/LayoutPrimitivesShowcases.tsx +129 -114
- package/src/showcases/NavigationShowcases.tsx +45 -37
- package/src/showcases/PageShowcases.tsx +171 -152
- package/src/showcases/ProductoDetalle.tsx +272 -129
- package/src/showcases/ShowcaseLayout.tsx +256 -219
- package/src/showcases/SizesShowcase.tsx +123 -64
- package/src/showcases/StructureShowcases.tsx +203 -138
- package/src/showcases/portfolioUi.tsx +153 -0
- package/src/showcases/techIcons.ts +3 -0
- package/src/utils/table.utils.ts +10 -2
- package/src/components/atoms/avatar/avatar.doc.mdx +0 -48
- package/src/components/atoms/avatar/avatar.stories.tsx +0 -64
- package/src/components/atoms/badget/badget.doc.mdx +0 -46
- package/src/components/atoms/badget/badget.stories.tsx +0 -75
- package/src/components/atoms/button/button.doc.mdx +0 -59
- package/src/components/atoms/button/button.stories.tsx +0 -173
- package/src/components/atoms/checkbox/checkbox.doc.mdx +0 -54
- package/src/components/atoms/checkbox/checkbox.stories.tsx +0 -53
- package/src/components/atoms/chip/chip.doc.mdx +0 -47
- package/src/components/atoms/chip/chip.stories.tsx +0 -64
- package/src/components/atoms/divider/divider.doc.mdx +0 -42
- package/src/components/atoms/divider/divider.stories.tsx +0 -33
- package/src/components/atoms/flex/flex.doc.mdx +0 -59
- package/src/components/atoms/flex/flex.stories.tsx +0 -70
- package/src/components/atoms/grid/grid.doc.mdx +0 -54
- package/src/components/atoms/grid/grid.stories.tsx +0 -71
- package/src/components/atoms/image/image.doc.mdx +0 -44
- package/src/components/atoms/image/image.stories.tsx +0 -48
- package/src/components/atoms/input/input.doc.mdx +0 -70
- package/src/components/atoms/input/input.stories.tsx +0 -113
- package/src/components/atoms/input-number/input-number.doc.mdx +0 -74
- package/src/components/atoms/input-number/input-number.stories.tsx +0 -111
- package/src/components/atoms/loader/loader.doc.mdx +0 -43
- package/src/components/atoms/loader/loader.stories.tsx +0 -104
- package/src/components/atoms/popover/popover.doc.mdx +0 -53
- package/src/components/atoms/popover/popover.stories.tsx +0 -24
- package/src/components/atoms/progress/progress.doc.mdx +0 -48
- package/src/components/atoms/progress/progress.stories.tsx +0 -39
- package/src/components/atoms/radio/radio.doc.mdx +0 -62
- package/src/components/atoms/radio/radio.stories.tsx +0 -48
- package/src/components/atoms/segmented-control/segmented-control.doc.mdx +0 -60
- package/src/components/atoms/segmented-control/segmented-control.stories.tsx +0 -62
- package/src/components/atoms/skeleton/skeleton.doc.mdx +0 -43
- package/src/components/atoms/skeleton/skeleton.stories.tsx +0 -29
- package/src/components/atoms/slide/slide.doc.mdx +0 -46
- package/src/components/atoms/slide/slide.stories.tsx +0 -120
- package/src/components/atoms/slider/slider.doc.mdx +0 -49
- package/src/components/atoms/slider/slider.stories.tsx +0 -29
- package/src/components/atoms/stack/stack.doc.mdx +0 -126
- package/src/components/atoms/stack/stack.stories.tsx +0 -283
- package/src/components/atoms/text/text.doc.mdx +0 -41
- package/src/components/atoms/text/text.stories.tsx +0 -66
- package/src/components/atoms/textarea/textarea.doc.mdx +0 -64
- package/src/components/atoms/textarea/textarea.stories.tsx +0 -26
- package/src/components/atoms/tooltip/tooltip.doc.mdx +0 -47
- package/src/components/atoms/tooltip/tooltip.stories.tsx +0 -19
- package/src/components/isolation/isolation.stories.tsx +0 -202
- package/src/components/molecules/accordion/accordion.doc.mdx +0 -47
- package/src/components/molecules/accordion/accordion.stories.tsx +0 -68
- package/src/components/molecules/alert/alert.doc.mdx +0 -48
- package/src/components/molecules/alert/alert.stories.tsx +0 -40
- package/src/components/molecules/breadcrumbs/breadcrumbs.doc.mdx +0 -57
- package/src/components/molecules/breadcrumbs/breadcrumbs.stories.tsx +0 -20
- package/src/components/molecules/calendar/calendar.doc.mdx +0 -76
- package/src/components/molecules/calendar/calendar.stories.tsx +0 -90
- package/src/components/molecules/card/card.doc.mdx +0 -54
- package/src/components/molecules/card/card.stories.tsx +0 -57
- package/src/components/molecules/chip-input/chip-input.doc.mdx +0 -44
- package/src/components/molecules/chip-input/chip-input.stories.tsx +0 -52
- package/src/components/molecules/date-picker/date-picker.doc.mdx +0 -65
- package/src/components/molecules/date-picker/date-picker.stories.tsx +0 -88
- package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +0 -46
- package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +0 -41
- package/src/components/molecules/empty-state/empty-state.doc.mdx +0 -50
- package/src/components/molecules/empty-state/empty-state.stories.tsx +0 -19
- package/src/components/molecules/field/field.doc.mdx +0 -43
- package/src/components/molecules/field/field.stories.tsx +0 -42
- package/src/components/molecules/form-header/form-header.doc.mdx +0 -38
- package/src/components/molecules/form-header/form-header.stories.tsx +0 -52
- package/src/components/molecules/masked-input/masked-input.doc.mdx +0 -78
- package/src/components/molecules/masked-input/masked-input.stories.tsx +0 -126
- package/src/components/molecules/multi-select/multi-select.doc.mdx +0 -55
- package/src/components/molecules/multi-select/multi-select.stories.tsx +0 -59
- package/src/components/molecules/pagination/pagination.doc.mdx +0 -62
- package/src/components/molecules/pagination/pagination.stories.tsx +0 -110
- package/src/components/molecules/search-select/search-select.doc.mdx +0 -77
- package/src/components/molecules/search-select/search-select.stories.tsx +0 -146
- package/src/components/molecules/select/select.doc.mdx +0 -75
- package/src/components/molecules/select/select.stories.tsx +0 -85
- package/src/components/molecules/stat-card/stat-card.doc.mdx +0 -48
- package/src/components/molecules/stat-card/stat-card.stories.tsx +0 -40
- package/src/components/molecules/stepper/stepper.doc.mdx +0 -54
- package/src/components/molecules/stepper/stepper.stories.tsx +0 -154
- package/src/components/molecules/table/table.doc.mdx +0 -140
- package/src/components/molecules/table/table.stories.tsx +0 -402
- package/src/components/molecules/tabs/tabs.doc.mdx +0 -52
- package/src/components/molecules/tabs/tabs.stories.tsx +0 -31
- package/src/components/molecules/time-picker/timePicker.doc.mdx +0 -68
- package/src/components/molecules/time-picker/timePicker.stories.tsx +0 -130
- package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +0 -95
- package/src/components/organisms/confirm-dialog/confirm-dialog.doc.mdx +0 -60
- package/src/components/organisms/confirm-dialog/confirm-dialog.stories.tsx +0 -32
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +0 -132
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +0 -353
- package/src/components/organisms/dialog/dialog.doc.mdx +0 -54
- package/src/components/organisms/dialog/dialog.stories.tsx +0 -126
- package/src/components/organisms/drawer/drawer.doc.mdx +0 -59
- package/src/components/organisms/drawer/drawer.stories.tsx +0 -40
- package/src/components/organisms/dropfile/dropfile.doc.mdx +0 -74
- package/src/components/organisms/dropfile/dropfile.stories.tsx +0 -93
- package/src/components/organisms/form-builder/formBuilder.doc.mdx +0 -251
- package/src/components/organisms/form-builder/formBuilder.stories.tsx +0 -914
- package/src/components/organisms/navbar/navbar.doc.mdx +0 -77
- package/src/components/organisms/navbar/navbar.stories.tsx +0 -83
- package/src/components/organisms/page-header/page-header.doc.mdx +0 -54
- package/src/components/organisms/page-header/page-header.stories.tsx +0 -74
- package/src/components/organisms/searchTable/searchTable.doc.mdx +0 -79
- package/src/components/organisms/searchTable/searchTable.stories.tsx +0 -73
- package/src/components/organisms/sidebar/sidebar.doc.mdx +0 -176
- package/src/components/organisms/sidebar/sidebar.stories.tsx +0 -234
- package/src/components/organisms/toast/toast.doc.mdx +0 -50
- package/src/components/organisms/toast/toast.stories.tsx +0 -137
- package/src/components/organisms/topbar/topbar.doc.mdx +0 -72
- package/src/components/organisms/topbar/topbar.stories.tsx +0 -79
- package/src/components/organisms/triple-filter/tripleFilter.doc.mdx +0 -62
- package/src/components/organisms/triple-filter/tripleFilter.stories.tsx +0 -31
- package/src/components/templates/layout/layout.doc.mdx +0 -53
- package/src/components/templates/layout/layout.stories.tsx +0 -113
- package/src/components/templates/page/page.doc.mdx +0 -86
- package/src/components/templates/page/page.stories.tsx +0 -97
- package/src/components/theme-provider/themeProvider.doc.mdx +0 -85
- package/src/components/theme-provider/themeProvider.stories.tsx +0 -58
|
@@ -1,284 +1,359 @@
|
|
|
1
|
-
import { useState, type ReactNode } from "react";
|
|
1
|
+
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
FaCode,
|
|
6
|
-
FaDatabase,
|
|
3
|
+
FaArrowRight,
|
|
4
|
+
FaCheck,
|
|
7
5
|
FaDownload,
|
|
8
6
|
FaExternalLinkAlt,
|
|
9
7
|
FaGithub,
|
|
10
|
-
FaLayerGroup,
|
|
11
8
|
FaLinkedin,
|
|
12
|
-
FaMapMarkerAlt,
|
|
13
|
-
FaMedium,
|
|
14
|
-
FaMobileAlt,
|
|
15
9
|
FaSearch,
|
|
16
|
-
FaServer,
|
|
17
10
|
} from "react-icons/fa";
|
|
18
|
-
import ITBadget from "../components/atoms/badget/badget";
|
|
19
|
-
import ITButton from "../components/atoms/button/button";
|
|
20
|
-
import ITDivider from "../components/atoms/divider/divider";
|
|
21
11
|
import ITFlex from "../components/atoms/flex/flex";
|
|
22
12
|
import ITGrid from "../components/atoms/grid/grid";
|
|
23
|
-
import ITStack from "../components/atoms/stack/stack";
|
|
24
13
|
import ITText from "../components/atoms/text/text";
|
|
25
14
|
import { techIcon, type TechIconEntry } from "./techIcons";
|
|
26
15
|
import { PRODUCTOS, type Producto } from "./productos";
|
|
27
|
-
import ProductoDetalle from "./ProductoDetalle";
|
|
28
16
|
import ProductoIcono from "./ProductoIcono";
|
|
17
|
+
import {
|
|
18
|
+
BarraProgreso,
|
|
19
|
+
CARD,
|
|
20
|
+
ControlesPortafolio,
|
|
21
|
+
CARD_HOVER,
|
|
22
|
+
CHIP,
|
|
23
|
+
EYEBROW,
|
|
24
|
+
EnlaceBoton,
|
|
25
|
+
Etiqueta,
|
|
26
|
+
SaltarContenido,
|
|
27
|
+
VolverArriba,
|
|
28
|
+
} from "./portfolioUi";
|
|
29
|
+
import { useITTranslation } from "@/i18n";
|
|
29
30
|
|
|
30
31
|
const CV_URL = "/Martin_Amaro_CV_ES_2026.pdf";
|
|
31
32
|
const GITHUB_URL = "https://github.com/maag070208";
|
|
32
33
|
const LINKEDIN_URL = "https://www.linkedin.com/in/maag070208/";
|
|
33
34
|
const MEDIUM_URL = "https://medium.com/@axzydev";
|
|
35
|
+
const SANDBOX_URL = "#ui-system";
|
|
36
|
+
|
|
37
|
+
/** Producto que ocupa el bloque destacado del portafolio. */
|
|
38
|
+
const DESTACADO = "checkapp";
|
|
39
|
+
|
|
40
|
+
/** Ancho de la hoja: una sola columna de lectura, sin cajas flotantes. */
|
|
41
|
+
const HOJA = "mx-auto w-full max-w-[1180px] px-5 sm:px-8 lg:px-10";
|
|
42
|
+
|
|
43
|
+
/** Anchor ids of the portfolio sections, in document order (drives the scroll-spy). */
|
|
44
|
+
const SECTION_IDS = ["sobre-mi", "stack", "proyectos", "contacto"] as const;
|
|
45
|
+
|
|
46
|
+
/** Top navigation entries: anchor id → i18n key. El orden es el número de sección. */
|
|
47
|
+
const NAV_ITEMS: { id: (typeof SECTION_IDS)[number]; key: string }[] = [
|
|
48
|
+
{ id: "sobre-mi", key: "showcase.home.navAbout" },
|
|
49
|
+
{ id: "stack", key: "showcase.home.navStack" },
|
|
50
|
+
{ id: "proyectos", key: "showcase.home.navProjects" },
|
|
51
|
+
{ id: "contacto", key: "showcase.home.navContact" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** Cabecera sticky: en móvil ocupa dos filas, así que el ancla deja más aire. */
|
|
55
|
+
const ANCLA = "scroll-mt-[6.75rem] md:scroll-mt-[4.5rem]";
|
|
56
|
+
|
|
57
|
+
/** Shared stagger offset for the hero entrance cascade (`it-reveal`). */
|
|
58
|
+
const reveal = (delay: number): CSSProperties =>
|
|
59
|
+
({ "--it-delay": `${delay}ms` }) as CSSProperties;
|
|
60
|
+
|
|
61
|
+
/** Filete de 1px: la separación entre bloques (no hay tarjetas contenedoras). */
|
|
62
|
+
const FILETE = "border-slate-200 dark:border-slate-800";
|
|
63
|
+
|
|
64
|
+
/** Índice de sección en mono: `01`, `02`… */
|
|
65
|
+
const numero = (index: number): string => String(index + 1).padStart(2, "0");
|
|
34
66
|
|
|
35
67
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* posición.
|
|
68
|
+
* Tracks which anchored section is currently in view (scroll-spy) so the top
|
|
69
|
+
* navigation can highlight the active link.
|
|
39
70
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
function useActiveSection(ids: readonly string[]): string {
|
|
72
|
+
const [active, setActive] = useState(ids[0]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const elements = ids
|
|
75
|
+
.map((id) => document.getElementById(id))
|
|
76
|
+
.filter((el): el is HTMLElement => Boolean(el));
|
|
77
|
+
if (elements.length === 0) return;
|
|
78
|
+
|
|
79
|
+
const observer = new IntersectionObserver(
|
|
80
|
+
(entries) => {
|
|
81
|
+
const visible = entries
|
|
82
|
+
.filter((entry) => entry.isIntersecting)
|
|
83
|
+
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
|
84
|
+
if (visible[0]) setActive(visible[0].target.id);
|
|
85
|
+
},
|
|
86
|
+
{ rootMargin: "-35% 0px -55% 0px", threshold: 0 }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
elements.forEach((el) => observer.observe(el));
|
|
90
|
+
return () => observer.disconnect();
|
|
91
|
+
}, [ids]);
|
|
92
|
+
return active;
|
|
93
|
+
}
|
|
46
94
|
|
|
47
95
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
96
|
+
* Cabecera: marca, índice de secciones con scroll-spy, progreso de lectura y el
|
|
97
|
+
* único punto de entrada al sandbox de componentes (`#ui-system`).
|
|
98
|
+
*/
|
|
99
|
+
const PortfolioNav = () => {
|
|
100
|
+
const { t } = useITTranslation();
|
|
101
|
+
const active = useActiveSection(SECTION_IDS);
|
|
102
|
+
|
|
103
|
+
const linkClass = (id: string) =>
|
|
104
|
+
`inline-flex items-baseline gap-1.5 whitespace-nowrap rounded-[6px] px-2.5 py-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] transition-colors ${
|
|
105
|
+
active === id
|
|
106
|
+
? "text-primary-700 dark:text-primary-500"
|
|
107
|
+
: "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
|
108
|
+
}`;
|
|
109
|
+
|
|
110
|
+
const enlaces = NAV_ITEMS.map((item, index) => (
|
|
111
|
+
<li key={item.id}>
|
|
112
|
+
<a
|
|
113
|
+
href={`#${item.id}`}
|
|
114
|
+
aria-current={active === item.id ? "location" : undefined}
|
|
115
|
+
className={linkClass(item.id)}
|
|
116
|
+
>
|
|
117
|
+
<span className="tabular-nums">{numero(index)}</span>
|
|
118
|
+
{t(item.key)}
|
|
119
|
+
</a>
|
|
120
|
+
</li>
|
|
121
|
+
));
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<header className="sticky top-0 z-[100] border-b border-slate-200 bg-white/85 backdrop-blur-md dark:border-slate-800 dark:bg-[#0b0d10]/85">
|
|
125
|
+
<nav aria-label={t("showcase.home.sectionNav")} className={HOJA}>
|
|
126
|
+
<div className="flex h-14 items-center gap-4">
|
|
127
|
+
<a href="#sobre-mi" className="flex items-center gap-2.5">
|
|
128
|
+
<span className="grid h-6 w-6 place-items-center rounded-[6px] bg-primary-700 font-mono !text-[10px] !font-bold text-white">
|
|
129
|
+
AA
|
|
130
|
+
</span>
|
|
131
|
+
<ITText
|
|
132
|
+
as="span"
|
|
133
|
+
className="hidden font-display !text-[13px] !font-bold -tracking-[0.01em] text-slate-900 dark:text-white sm:block"
|
|
134
|
+
>
|
|
135
|
+
Asael Amaro
|
|
136
|
+
</ITText>
|
|
137
|
+
</a>
|
|
138
|
+
|
|
139
|
+
<ul className="ml-auto hidden items-center gap-1 md:flex">{enlaces}</ul>
|
|
140
|
+
|
|
141
|
+
<div className="ml-auto flex items-center gap-2 md:ml-3">
|
|
142
|
+
<ControlesPortafolio />
|
|
143
|
+
<EnlaceBoton
|
|
144
|
+
href={SANDBOX_URL}
|
|
145
|
+
variante="outlined"
|
|
146
|
+
tamano="sm"
|
|
147
|
+
icono={<FaArrowRight size={10} />}
|
|
148
|
+
>
|
|
149
|
+
{t("showcase.home.navUiSystem")}
|
|
150
|
+
</EnlaceBoton>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Móvil: el mismo índice en una fila desplazable */}
|
|
155
|
+
<ul className="flex items-center gap-1 overflow-x-auto pb-2 md:hidden">{enlaces}</ul>
|
|
156
|
+
</nav>
|
|
157
|
+
|
|
158
|
+
<BarraProgreso />
|
|
159
|
+
</header>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Encabezado de sección: número mono + título display + apoyo, con acciones a la
|
|
165
|
+
* derecha. El bloque lo separa el filete superior de cada sección.
|
|
50
166
|
*/
|
|
51
167
|
const SectionHeading = ({
|
|
52
|
-
|
|
168
|
+
index,
|
|
169
|
+
label,
|
|
53
170
|
title,
|
|
54
171
|
subtitle,
|
|
55
|
-
|
|
172
|
+
actions,
|
|
56
173
|
}: {
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
/**
|
|
174
|
+
/** Índice de sección (0-based: se pinta con dos dígitos). */
|
|
175
|
+
index: number;
|
|
176
|
+
/** Etiqueta mono corta. */
|
|
177
|
+
label: string;
|
|
178
|
+
/** Título display. */
|
|
60
179
|
title: string;
|
|
61
|
-
/** Frase de apoyo
|
|
180
|
+
/** Frase de apoyo. */
|
|
62
181
|
subtitle?: string;
|
|
63
|
-
/**
|
|
64
|
-
|
|
182
|
+
/** Contenido alineado a la derecha (filtros). */
|
|
183
|
+
actions?: ReactNode;
|
|
65
184
|
}) => (
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{title}
|
|
72
|
-
</ITText>
|
|
73
|
-
{subtitle && (
|
|
74
|
-
<ITText as="p" className="!text-sm text-slate-500 dark:text-slate-400">
|
|
75
|
-
{subtitle}
|
|
185
|
+
<div className="flex flex-col gap-5 pb-7 md:flex-row md:items-end md:justify-between">
|
|
186
|
+
<div className="min-w-0">
|
|
187
|
+
<ITText as="p" className={EYEBROW}>
|
|
188
|
+
<span className="tabular-nums">{numero(index)}</span>
|
|
189
|
+
{` — ${label}`}
|
|
76
190
|
</ITText>
|
|
77
|
-
|
|
78
|
-
|
|
191
|
+
<ITText
|
|
192
|
+
as="h2"
|
|
193
|
+
className="font-display !mt-2.5 !text-[26px] !font-bold -tracking-[0.02em] text-slate-900 dark:text-white sm:!text-[32px]"
|
|
194
|
+
>
|
|
195
|
+
{title}
|
|
196
|
+
</ITText>
|
|
197
|
+
{subtitle && (
|
|
198
|
+
<ITText
|
|
199
|
+
as="p"
|
|
200
|
+
className="!mt-2 max-w-[62ch] !text-[15px] !leading-relaxed text-slate-500 dark:text-slate-400"
|
|
201
|
+
>
|
|
202
|
+
{subtitle}
|
|
203
|
+
</ITText>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
{actions && <div className="shrink-0">{actions}</div>}
|
|
207
|
+
</div>
|
|
79
208
|
);
|
|
80
209
|
|
|
81
210
|
/**
|
|
82
|
-
* Ícono de tecnología local
|
|
83
|
-
*
|
|
211
|
+
* Ícono de tecnología local. Es decorativo (el nombre va al lado), así que se
|
|
212
|
+
* oculta a lectores de pantalla y solo conserva el `title` nativo.
|
|
84
213
|
*/
|
|
85
|
-
const TechIconImg = ({
|
|
86
|
-
icon,
|
|
87
|
-
alt,
|
|
88
|
-
size = "w-7 h-7",
|
|
89
|
-
}: {
|
|
90
|
-
/** Ícono resuelto por `techIcons`. */
|
|
91
|
-
icon: TechIconEntry;
|
|
92
|
-
/** Texto alternativo. */
|
|
93
|
-
alt: string;
|
|
94
|
-
/** Clases de tamaño. @default "w-7 h-7" */
|
|
95
|
-
size?: string;
|
|
96
|
-
}) => (
|
|
214
|
+
const TechIconImg = ({ icon, alt }: { icon: TechIconEntry; alt: string }) => (
|
|
97
215
|
<img
|
|
98
216
|
src={icon.src}
|
|
99
|
-
alt=
|
|
100
|
-
|
|
217
|
+
alt=""
|
|
218
|
+
aria-hidden
|
|
219
|
+
title={alt}
|
|
220
|
+
className={`h-4 w-4 object-contain ${icon.mono ? "dark:invert" : ""}`}
|
|
101
221
|
loading="lazy"
|
|
102
222
|
/>
|
|
103
223
|
);
|
|
104
224
|
|
|
105
|
-
/** Tarjeta de una tecnología: ícono en caja blanca + nombre. */
|
|
106
|
-
const TechTile = ({ icon, label }: { icon: TechIconEntry; label: string }) => (
|
|
107
|
-
<div className="group flex h-full flex-col items-center justify-center gap-2 rounded-xl border border-slate-100 bg-slate-50/70 p-3 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary-300 hover:bg-white hover:shadow-md dark:border-slate-800 dark:bg-slate-950/40 dark:hover:border-primary-700 dark:hover:bg-slate-900">
|
|
108
|
-
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-white shadow-sm dark:bg-slate-900">
|
|
109
|
-
<TechIconImg icon={icon} alt={label} size="w-7 h-7" />
|
|
110
|
-
</div>
|
|
111
|
-
<ITText as="span" className="w-full truncate text-center text-[11px] font-semibold text-slate-600 dark:text-slate-300">
|
|
112
|
-
{label}
|
|
113
|
-
</ITText>
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
/** Métrica del hero: valor grande, ícono con degradado y texto de apoyo. */
|
|
118
|
-
const StatCard = ({ stat, index }: { stat: { label: string; value: string; hint: string; icon: ReactNode }; index: number }) => (
|
|
119
|
-
<div className="flex h-full flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:border-primary-300 hover:shadow-lg dark:border-slate-800 dark:bg-slate-900 dark:hover:border-primary-700">
|
|
120
|
-
<div className="flex items-start justify-between gap-3">
|
|
121
|
-
<ITText as="p" className="!text-3xl !font-extrabold !leading-none !tracking-tight tabular-nums text-slate-900 dark:text-white">
|
|
122
|
-
{stat.value}
|
|
123
|
-
</ITText>
|
|
124
|
-
<span className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-[13px] text-white shadow-sm ${ACCENTS[index % ACCENTS.length]}`}>
|
|
125
|
-
{stat.icon}
|
|
126
|
-
</span>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
<div className="mt-auto">
|
|
130
|
-
<ITText as="p" className="!text-sm !font-semibold text-primary-600 dark:text-primary-400">
|
|
131
|
-
{stat.label}
|
|
132
|
-
</ITText>
|
|
133
|
-
<ITText as="p" className="!mt-0.5 !text-[11px] text-slate-400 dark:text-slate-500">
|
|
134
|
-
{stat.hint}
|
|
135
|
-
</ITText>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
);
|
|
139
|
-
|
|
140
225
|
/**
|
|
141
|
-
* Tarjeta de producto
|
|
142
|
-
*
|
|
226
|
+
* Tarjeta de producto de la rejilla: portada, índice + tipo, nombre, descripción
|
|
227
|
+
* acotada, stack y acciones.
|
|
143
228
|
*/
|
|
144
|
-
const TarjetaProducto = ({
|
|
145
|
-
|
|
146
|
-
onVerDetalle,
|
|
147
|
-
}: {
|
|
148
|
-
producto: Producto;
|
|
149
|
-
onVerDetalle: (producto: Producto) => void;
|
|
150
|
-
}) => {
|
|
229
|
+
const TarjetaProducto = ({ producto, index }: { producto: Producto; index: number }) => {
|
|
230
|
+
const { t } = useITTranslation();
|
|
151
231
|
const capturas = producto.capturas ?? [];
|
|
152
232
|
const portada = capturas[0];
|
|
153
233
|
const tieneDetalle = capturas.length > 0;
|
|
154
234
|
const enlaceInterno = producto.enlace?.startsWith("#");
|
|
235
|
+
const stackVisible = producto.stack.slice(0, 3);
|
|
236
|
+
const stackResto = producto.stack.length - stackVisible.length;
|
|
155
237
|
|
|
156
238
|
return (
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
<button
|
|
160
|
-
type="button"
|
|
161
|
-
disabled={!tieneDetalle}
|
|
162
|
-
onClick={() => onVerDetalle(producto)}
|
|
163
|
-
aria-label={tieneDetalle ? `Ver detalle de ${producto.nombre}` : producto.nombre}
|
|
164
|
-
className="relative block aspect-[16/10] w-full overflow-hidden rounded-xl border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950/60"
|
|
165
|
-
>
|
|
239
|
+
<article className={`group flex h-full flex-col ${CARD} ${CARD_HOVER}`}>
|
|
240
|
+
<div className="relative flex aspect-[16/10] w-full items-center justify-center overflow-hidden border-b border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950">
|
|
166
241
|
{portada ? (
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
className="
|
|
171
|
-
|
|
172
|
-
|
|
242
|
+
<a
|
|
243
|
+
href={`#proyecto/${producto.id}`}
|
|
244
|
+
aria-label={t("showcase.home.viewDetailOf", { name: producto.nombre })}
|
|
245
|
+
className="absolute inset-0"
|
|
246
|
+
>
|
|
247
|
+
<img
|
|
248
|
+
src={portada.src}
|
|
249
|
+
alt={`${producto.nombre}: ${portada.titulo}`}
|
|
250
|
+
className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-[1.02]"
|
|
251
|
+
loading="lazy"
|
|
252
|
+
/>
|
|
253
|
+
</a>
|
|
173
254
|
) : (
|
|
174
|
-
<
|
|
175
|
-
<ProductoIcono icono={producto.icono} size={
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{capturas.length > 1 && (
|
|
180
|
-
<span className="absolute bottom-2 right-2 rounded-full bg-slate-900/70 px-2.5 py-1 text-[10px] font-semibold text-white backdrop-blur-sm">
|
|
181
|
-
{capturas.length} capturas
|
|
255
|
+
<span className="flex flex-col items-center gap-2 text-primary-700 dark:text-primary-500">
|
|
256
|
+
<ProductoIcono icono={producto.icono} size={30} />
|
|
257
|
+
<ITText as="span" className={EYEBROW}>
|
|
258
|
+
{producto.tipo}
|
|
259
|
+
</ITText>
|
|
182
260
|
</span>
|
|
183
261
|
)}
|
|
184
|
-
</
|
|
262
|
+
</div>
|
|
185
263
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<ProductoIcono icono={producto.icono} size={16} />
|
|
191
|
-
</div>
|
|
192
|
-
<ITText as="h3" className="!text-base !font-bold !tracking-tight text-slate-900 dark:text-white">
|
|
193
|
-
{producto.nombre}
|
|
264
|
+
<div className="flex flex-1 flex-col p-5">
|
|
265
|
+
<div className="flex items-center justify-between gap-3">
|
|
266
|
+
<ITText as="span" className={`${EYEBROW} tabular-nums`}>
|
|
267
|
+
{numero(index)}
|
|
194
268
|
</ITText>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
</ITFlex>
|
|
269
|
+
<Etiqueta>{producto.tipo}</Etiqueta>
|
|
270
|
+
</div>
|
|
198
271
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
272
|
+
<ITText
|
|
273
|
+
as="h3"
|
|
274
|
+
className="font-display !mt-3 !text-[19px] !font-bold -tracking-[0.02em] text-slate-900 dark:text-white"
|
|
275
|
+
>
|
|
276
|
+
{producto.nombre}
|
|
277
|
+
</ITText>
|
|
202
278
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{tecnologia}
|
|
210
|
-
</span>
|
|
211
|
-
))}
|
|
212
|
-
</ITFlex>
|
|
213
|
-
|
|
214
|
-
{/* Acciones */}
|
|
215
|
-
<ITFlex align="center" justify="between" gap={3} wrap="wrap" className="mt-auto pt-2">
|
|
216
|
-
{tieneDetalle ? (
|
|
217
|
-
<ITButton variant="filled" color="primary" size="sm" onClick={() => onVerDetalle(producto)}>
|
|
218
|
-
Ver detalle
|
|
219
|
-
</ITButton>
|
|
220
|
-
) : (
|
|
221
|
-
<ITText as="span" />
|
|
222
|
-
)}
|
|
279
|
+
<ITText
|
|
280
|
+
as="p"
|
|
281
|
+
className="line-clamp-3 !mt-2 !text-[13.5px] !leading-relaxed text-slate-600 dark:text-slate-300"
|
|
282
|
+
>
|
|
283
|
+
{producto.descripcion}
|
|
284
|
+
</ITText>
|
|
223
285
|
|
|
224
|
-
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
286
|
+
<ITFlex gap={1.5} wrap="wrap" className="!mt-4">
|
|
287
|
+
{stackVisible.map((tecnologia) => (
|
|
288
|
+
<span key={tecnologia} className={CHIP}>
|
|
289
|
+
{tecnologia}
|
|
290
|
+
</span>
|
|
291
|
+
))}
|
|
292
|
+
{stackResto > 0 && (
|
|
293
|
+
<span className={CHIP}>{t("showcase.home.moreStack", { count: stackResto })}</span>
|
|
294
|
+
)}
|
|
295
|
+
</ITFlex>
|
|
296
|
+
|
|
297
|
+
<div className="mt-auto flex items-center justify-between gap-3 pt-5">
|
|
298
|
+
{tieneDetalle ? (
|
|
299
|
+
<EnlaceBoton href={`#proyecto/${producto.id}`} tamano="sm">
|
|
300
|
+
{t("showcase.home.viewDetail")}
|
|
301
|
+
</EnlaceBoton>
|
|
302
|
+
) : (
|
|
303
|
+
<ITText as="span" className={EYEBROW}>
|
|
304
|
+
{t("showcase.home.noPreview")}
|
|
305
|
+
</ITText>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{producto.enlace && (
|
|
309
|
+
<a
|
|
310
|
+
href={producto.enlace}
|
|
311
|
+
{...(enlaceInterno ? {} : { target: "_blank", rel: "noopener noreferrer" })}
|
|
312
|
+
className="inline-flex items-center gap-1.5 !text-xs !font-semibold !text-primary-700 dark:!text-primary-500"
|
|
313
|
+
>
|
|
314
|
+
{!enlaceInterno && <FaExternalLinkAlt size={10} />}
|
|
315
|
+
{producto.enlaceTexto ?? t("showcase.home.viewRepo")}
|
|
316
|
+
</a>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</article>
|
|
236
321
|
);
|
|
237
322
|
};
|
|
238
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Portafolio (`/`): hoja técnica con masthead tipográfico, banda de métricas,
|
|
326
|
+
* stack en formato datasheet y productos (uno destacado + rejilla).
|
|
327
|
+
*
|
|
328
|
+
* No recibe props: cada tarjeta enlaza a `#proyecto/<id>` con un `<a>` real, así
|
|
329
|
+
* que el router por hash hace la navegación (y el detalle es compartible).
|
|
330
|
+
*/
|
|
239
331
|
export const HomeShowcase = () => {
|
|
332
|
+
const { t } = useITTranslation();
|
|
240
333
|
const [techSearch, setTechSearch] = useState("");
|
|
241
|
-
const [selectedCategory, setSelectedCategory] = useState("all");
|
|
242
|
-
const [detalle, setDetalle] = useState<Producto | null>(null);
|
|
243
|
-
|
|
244
|
-
const LANGUAGES = [
|
|
245
|
-
{ name: "TypeScript", icon: techIcon("typescript") },
|
|
246
|
-
{ name: "JavaScript", icon: techIcon("javascript") },
|
|
247
|
-
{ name: "C#", icon: techIcon("csharp") },
|
|
248
|
-
{ name: "Python", icon: techIcon("python") },
|
|
249
|
-
{ name: "Kotlin", icon: techIcon("kotlin") },
|
|
250
|
-
{ name: "Dart", icon: techIcon("dart") },
|
|
251
|
-
];
|
|
252
334
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
{ name: "
|
|
256
|
-
{ name: "
|
|
257
|
-
{ name: "
|
|
258
|
-
{ name: "
|
|
259
|
-
{ name: "
|
|
260
|
-
{ name: "
|
|
261
|
-
{ name: "Electron", icon: techIcon("electron") },
|
|
262
|
-
];
|
|
263
|
-
|
|
264
|
-
const TECH_STACK = [
|
|
265
|
-
{ name: "TypeScript", icon: techIcon("typescript"), category: "frontend" },
|
|
266
|
-
{ name: "JavaScript", icon: techIcon("javascript"), category: "frontend" },
|
|
335
|
+
/** Stack completo: única fuente de verdad (el datasheet lo agrupa por categoría). */
|
|
336
|
+
const TECH_STACK: { name: string; icon: TechIconEntry; category: string }[] = [
|
|
337
|
+
{ name: "TypeScript", icon: techIcon("typescript"), category: "languages" },
|
|
338
|
+
{ name: "JavaScript", icon: techIcon("javascript"), category: "languages" },
|
|
339
|
+
{ name: "C#", icon: techIcon("csharp"), category: "languages" },
|
|
340
|
+
{ name: "Python", icon: techIcon("python"), category: "languages" },
|
|
341
|
+
{ name: "Kotlin", icon: techIcon("kotlin"), category: "languages" },
|
|
342
|
+
{ name: "Dart", icon: techIcon("dart"), category: "languages" },
|
|
267
343
|
{ name: "Angular", icon: techIcon("angular"), category: "frontend" },
|
|
268
344
|
{ name: "React", icon: techIcon("react"), category: "frontend" },
|
|
269
345
|
{ name: "Redux", icon: techIcon("redux"), category: "frontend" },
|
|
270
346
|
{ name: "HTML5", icon: techIcon("html5"), category: "frontend" },
|
|
271
347
|
{ name: "CSS3", icon: techIcon("css3"), category: "frontend" },
|
|
272
348
|
{ name: "Sass", icon: techIcon("sass"), category: "frontend" },
|
|
273
|
-
{ name: "C#", icon: techIcon("csharp"), category: "backend" },
|
|
274
|
-
{ name: "Python", icon: techIcon("python"), category: "backend" },
|
|
275
349
|
{ name: "Node.js", icon: techIcon("nodejs"), category: "backend" },
|
|
276
350
|
{ name: "Express", icon: techIcon("express"), category: "backend" },
|
|
277
351
|
{ name: ".NET Core", icon: techIcon("dotnet"), category: "backend" },
|
|
352
|
+
{ name: "Spring Boot", icon: techIcon("springboot"), category: "backend" },
|
|
278
353
|
{ name: "MongoDB", icon: techIcon("mongodb"), category: "database" },
|
|
279
354
|
{ name: "MySQL", icon: techIcon("mysql"), category: "database" },
|
|
355
|
+
{ name: "PostgreSQL", icon: techIcon("postgresql"), category: "database" },
|
|
280
356
|
{ name: "Flutter", icon: techIcon("flutter"), category: "mobile" },
|
|
281
|
-
{ name: "Kotlin", icon: techIcon("kotlin"), category: "mobile" },
|
|
282
357
|
{ name: "Android", icon: techIcon("android"), category: "mobile" },
|
|
283
358
|
{ name: "Ionic", icon: techIcon("ionic"), category: "mobile" },
|
|
284
359
|
{ name: "Docker", icon: techIcon("docker"), category: "devops" },
|
|
@@ -289,329 +364,450 @@ export const HomeShowcase = () => {
|
|
|
289
364
|
{ name: "Electron", icon: techIcon("electron"), category: "desktop" },
|
|
290
365
|
{ name: "Visual Studio", icon: techIcon("visualstudio"), category: "tools" },
|
|
291
366
|
{ name: "Jest", icon: techIcon("jest"), category: "testing" },
|
|
367
|
+
{ name: "Appium", icon: techIcon("appium"), category: "testing" },
|
|
292
368
|
];
|
|
293
369
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
{
|
|
297
|
-
{
|
|
298
|
-
{
|
|
370
|
+
/** Orden y etiqueta i18n de cada fila del datasheet. */
|
|
371
|
+
const FILAS: { category: string; key: string }[] = [
|
|
372
|
+
{ category: "languages", key: "showcase.home.catLanguages" },
|
|
373
|
+
{ category: "frontend", key: "showcase.home.catFrontend" },
|
|
374
|
+
{ category: "backend", key: "showcase.home.catBackend" },
|
|
375
|
+
{ category: "database", key: "showcase.home.catDatabase" },
|
|
376
|
+
{ category: "mobile", key: "showcase.home.catMobile" },
|
|
377
|
+
{ category: "devops", key: "showcase.home.catDevops" },
|
|
378
|
+
{ category: "desktop", key: "showcase.home.catDesktop" },
|
|
379
|
+
{ category: "tools", key: "showcase.home.catTools" },
|
|
380
|
+
{ category: "testing", key: "showcase.home.catTesting" },
|
|
299
381
|
];
|
|
300
382
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
383
|
+
const termino = techSearch.trim().toLowerCase();
|
|
384
|
+
const filas = FILAS.map((fila) => ({
|
|
385
|
+
...fila,
|
|
386
|
+
items: TECH_STACK.filter(
|
|
387
|
+
(tech) => tech.category === fila.category && tech.name.toLowerCase().includes(termino)
|
|
388
|
+
),
|
|
389
|
+
})).filter((fila) => fila.items.length > 0);
|
|
390
|
+
const visibles = filas.reduce((total, fila) => total + fila.items.length, 0);
|
|
391
|
+
|
|
392
|
+
/** Métricas del perfil: una banda de datos con filetes, no cuatro tarjetas. */
|
|
393
|
+
const METRICAS = [
|
|
394
|
+
{ label: t("showcase.home.statExperience"), value: "5+" },
|
|
395
|
+
{ label: t("showcase.home.statProducts"), value: `${PRODUCTOS.length}` },
|
|
396
|
+
{ label: t("showcase.home.statTech"), value: `${TECH_STACK.length}` },
|
|
397
|
+
{ label: t("showcase.home.statMode"), value: t("showcase.home.statModeValue") },
|
|
308
398
|
];
|
|
309
399
|
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
});
|
|
400
|
+
const destacado = PRODUCTOS.find((producto) => producto.id === DESTACADO) ?? PRODUCTOS[0];
|
|
401
|
+
const portadaDestacada = destacado.capturas?.[0];
|
|
402
|
+
const resto = PRODUCTOS.filter((producto) => producto.id !== destacado.id);
|
|
403
|
+
const enlaceDestacadoExterno = Boolean(destacado.enlace && !destacado.enlace.startsWith("#"));
|
|
315
404
|
|
|
316
405
|
return (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
aria-hidden
|
|
326
|
-
className="it-landing-float pointer-events-none absolute -bottom-24 -left-16 h-72 w-72 rounded-full bg-purple-400/20 blur-3xl"
|
|
327
|
-
style={{ animationDelay: "1.5s" }}
|
|
328
|
-
/>
|
|
329
|
-
<div aria-hidden className="it-landing-grid pointer-events-none absolute inset-0 text-slate-400/50 dark:text-slate-600/40" />
|
|
330
|
-
|
|
331
|
-
<div className="relative z-10 flex flex-col gap-12 xl:flex-row xl:items-center xl:gap-14">
|
|
332
|
-
<div className="min-w-0 max-w-3xl flex-1 xl:max-w-2xl">
|
|
333
|
-
<div className="it-landing-rise">
|
|
334
|
-
<ITBadget label="Portafolio · 2026" color="primary" variant="outlined" className="w-fit" />
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<ITText
|
|
338
|
-
as="h1"
|
|
339
|
-
id="home-hero-title"
|
|
340
|
-
className="it-landing-rise !mt-5 !text-4xl !font-extrabold !tracking-tight text-slate-900 dark:text-white sm:!text-5xl"
|
|
341
|
-
style={{ animationDelay: "70ms" }}
|
|
342
|
-
>
|
|
343
|
-
Hola, soy{" "}
|
|
344
|
-
<span className="bg-gradient-to-r from-primary-600 via-primary-500 to-purple-500 bg-clip-text text-transparent">
|
|
345
|
-
Asael Amaro
|
|
346
|
-
</span>
|
|
347
|
-
</ITText>
|
|
348
|
-
|
|
349
|
-
<ITText
|
|
350
|
-
as="p"
|
|
351
|
-
className="it-landing-rise !mt-4 !text-base !leading-relaxed max-w-xl text-slate-600 dark:text-slate-300"
|
|
352
|
-
style={{ animationDelay: "140ms" }}
|
|
353
|
-
>
|
|
354
|
-
Fullstack Developer con 4+ años creando aplicaciones web modernas.
|
|
355
|
-
Especializado en TypeScript, Angular y React, con foco en arquitectura
|
|
356
|
-
limpia y en productos que la gente usa todos los días.
|
|
357
|
-
</ITText>
|
|
358
|
-
|
|
406
|
+
<>
|
|
407
|
+
<SaltarContenido />
|
|
408
|
+
<PortfolioNav />
|
|
409
|
+
|
|
410
|
+
<main id="contenido" className="animate-fadeIn">
|
|
411
|
+
{/* ─── 01 · MASTHEAD ─── */}
|
|
412
|
+
<section id="sobre-mi" aria-labelledby="home-hero-title" className={ANCLA}>
|
|
413
|
+
<div className={`${HOJA} pb-10 pt-10 sm:pb-12 sm:pt-14`}>
|
|
359
414
|
<ITFlex
|
|
360
|
-
gap={3}
|
|
361
|
-
wrap="wrap"
|
|
362
415
|
align="center"
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
<ITButton variant="filled" color="primary" size="lg" icon={<FaDownload size={13} />}>
|
|
368
|
-
Descargar CV
|
|
369
|
-
</ITButton>
|
|
370
|
-
</a>
|
|
371
|
-
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer">
|
|
372
|
-
<ITButton variant="outlined" color="secondary" size="lg" icon={<FaGithub size={13} />}>
|
|
373
|
-
GitHub
|
|
374
|
-
</ITButton>
|
|
375
|
-
</a>
|
|
376
|
-
</ITFlex>
|
|
377
|
-
|
|
378
|
-
<div
|
|
379
|
-
className="it-landing-rise mt-7 inline-flex flex-wrap items-center gap-x-3 gap-y-2 rounded-2xl border border-primary-200 bg-gradient-to-r from-primary-50 to-purple-50 px-4 py-2 shadow-sm dark:border-primary-800/50 dark:from-primary-950/30 dark:to-purple-950/30"
|
|
380
|
-
style={{ animationDelay: "280ms" }}
|
|
416
|
+
justify="between"
|
|
417
|
+
gap={4}
|
|
418
|
+
wrap="wrap"
|
|
419
|
+
className="it-reveal border-b border-slate-200 pb-4 dark:border-slate-800"
|
|
381
420
|
>
|
|
382
|
-
<span className="
|
|
383
|
-
<span className="
|
|
384
|
-
|
|
421
|
+
<span className="inline-flex items-center gap-2.5">
|
|
422
|
+
<span className="relative flex h-1.5 w-1.5 shrink-0">
|
|
423
|
+
<span className="absolute inline-flex h-full w-full rounded-full bg-success-500 opacity-75" />
|
|
424
|
+
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-success-500" />
|
|
425
|
+
</span>
|
|
426
|
+
<ITText
|
|
427
|
+
as="span"
|
|
428
|
+
className="font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
|
|
429
|
+
>
|
|
430
|
+
{`${t("showcase.home.availableFullstack")} · ${t("showcase.home.location")}`}
|
|
431
|
+
</ITText>
|
|
385
432
|
</span>
|
|
386
|
-
<code className="text-sm font-medium text-slate-800 dark:text-slate-200">
|
|
387
|
-
Disponible para proyectos fullstack
|
|
388
|
-
</code>
|
|
389
433
|
<a
|
|
390
434
|
href={LINKEDIN_URL}
|
|
391
435
|
target="_blank"
|
|
392
436
|
rel="noopener noreferrer"
|
|
393
|
-
className="inline-flex items-center gap-1.5 text-
|
|
437
|
+
className="inline-flex items-center gap-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] !text-primary-700 dark:!text-primary-500"
|
|
394
438
|
>
|
|
395
|
-
<FaLinkedin size={
|
|
439
|
+
<FaLinkedin size={11} />
|
|
396
440
|
LinkedIn
|
|
397
441
|
</a>
|
|
398
|
-
</
|
|
399
|
-
</div>
|
|
442
|
+
</ITFlex>
|
|
400
443
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
444
|
+
<div className="mt-10 grid gap-10 lg:grid-cols-[minmax(0,1fr)_auto] lg:gap-14">
|
|
445
|
+
<div className="min-w-0">
|
|
446
|
+
<ITText
|
|
447
|
+
as="h1"
|
|
448
|
+
id="home-hero-title"
|
|
449
|
+
className="it-reveal font-display !text-[38px] !font-bold !leading-[1.06] -tracking-[0.015em] text-slate-900 dark:text-white sm:!text-[54px] lg:!text-[62px]"
|
|
450
|
+
style={reveal(60)}
|
|
451
|
+
>
|
|
452
|
+
{t("showcase.home.heroLead")}{" "}
|
|
453
|
+
<span className="text-primary-600 dark:text-primary-400">
|
|
454
|
+
{t("showcase.home.heroAccent")}
|
|
455
|
+
</span>
|
|
456
|
+
</ITText>
|
|
457
|
+
|
|
458
|
+
<ITText
|
|
459
|
+
as="p"
|
|
460
|
+
className="it-reveal !mt-6 max-w-[62ch] !text-[15.5px] !leading-[1.75] text-slate-600 dark:text-slate-300 sm:!text-[17px]"
|
|
461
|
+
style={reveal(130)}
|
|
462
|
+
>
|
|
463
|
+
{t("showcase.home.bio")}
|
|
464
|
+
</ITText>
|
|
465
|
+
|
|
466
|
+
<ITFlex
|
|
467
|
+
gap={3}
|
|
468
|
+
wrap="wrap"
|
|
469
|
+
align="center"
|
|
470
|
+
className="it-reveal !mt-8"
|
|
471
|
+
style={reveal(200)}
|
|
472
|
+
>
|
|
473
|
+
<EnlaceBoton href={CV_URL} descargar icono={<FaDownload size={12} />}>
|
|
474
|
+
{t("showcase.home.downloadCv")}
|
|
475
|
+
</EnlaceBoton>
|
|
476
|
+
<EnlaceBoton href={GITHUB_URL} variante="text" icono={<FaGithub size={13} />}>
|
|
477
|
+
GitHub
|
|
478
|
+
</EnlaceBoton>
|
|
479
|
+
<EnlaceBoton href={SANDBOX_URL} variante="text" icono={<FaArrowRight size={12} />}>
|
|
480
|
+
{t("showcase.home.navUiSystem")}
|
|
481
|
+
</EnlaceBoton>
|
|
482
|
+
</ITFlex>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{/* Retrato como documento: chico y con filete, sin tarjeta contenedora */}
|
|
486
|
+
<div
|
|
487
|
+
className="it-reveal flex items-start gap-4 lg:flex-col lg:gap-3 lg:self-end"
|
|
488
|
+
style={reveal(260)}
|
|
489
|
+
>
|
|
490
|
+
<img
|
|
491
|
+
src="/personalFoto.jpg"
|
|
492
|
+
alt="Asael Amaro"
|
|
493
|
+
className="h-[150px] w-[120px] rounded-[10px] border border-slate-200 object-cover dark:border-slate-800 lg:h-[210px] lg:w-[168px]"
|
|
494
|
+
/>
|
|
495
|
+
<div className="pt-0.5">
|
|
496
|
+
<ITText
|
|
497
|
+
as="p"
|
|
498
|
+
className="font-mono !text-[10px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
|
|
499
|
+
>
|
|
500
|
+
{t("showcase.home.role")}
|
|
416
501
|
</ITText>
|
|
417
|
-
<ITText as="p" className="!text-[11px] text-slate-
|
|
418
|
-
|
|
502
|
+
<ITText as="p" className="!mt-1 !text-[11px] text-slate-500 dark:text-slate-400">
|
|
503
|
+
{t("showcase.home.location")}
|
|
419
504
|
</ITText>
|
|
420
505
|
</div>
|
|
421
|
-
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-success-500/40 px-2.5 py-1 text-[10px] font-semibold text-success-600 dark:text-success-400">
|
|
422
|
-
<span className="h-1.5 w-1.5 rounded-full bg-success-500" />
|
|
423
|
-
Disponible
|
|
424
|
-
</span>
|
|
425
506
|
</div>
|
|
426
507
|
</div>
|
|
427
508
|
</div>
|
|
428
|
-
</div>
|
|
429
|
-
</section>
|
|
430
|
-
|
|
431
|
-
{/* ─── MÉTRICAS ─── */}
|
|
432
|
-
<section aria-label="Métricas del perfil">
|
|
433
|
-
<ITGrid container spacing={4}>
|
|
434
|
-
{STATS.map((stat, index) => (
|
|
435
|
-
<ITGrid item xs={6} lg={3} key={stat.label}>
|
|
436
|
-
<StatCard stat={stat} index={index} />
|
|
437
|
-
</ITGrid>
|
|
438
|
-
))}
|
|
439
|
-
</ITGrid>
|
|
440
|
-
</section>
|
|
441
|
-
|
|
442
|
-
{/* ─── LENGUAJES Y FRAMEWORKS ─── */}
|
|
443
|
-
<section>
|
|
444
|
-
<SectionHeading
|
|
445
|
-
eyebrow="Base técnica"
|
|
446
|
-
title="Lenguajes y frameworks"
|
|
447
|
-
subtitle="Con los que trabajo todos los días."
|
|
448
|
-
className="!mb-5"
|
|
449
|
-
/>
|
|
450
|
-
|
|
451
|
-
<ITGrid container spacing={5}>
|
|
452
|
-
<ITGrid item xs={12} lg={6}>
|
|
453
|
-
<div className="h-full rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
|
454
|
-
<ITText as="h3" className="!text-[11px] !font-bold uppercase !tracking-wider text-slate-400 dark:text-slate-500">
|
|
455
|
-
Lenguajes
|
|
456
|
-
</ITText>
|
|
457
|
-
<div className="mt-4 grid grid-cols-3 gap-3 sm:grid-cols-6 lg:grid-cols-3">
|
|
458
|
-
{LANGUAGES.map((lang) => (
|
|
459
|
-
<TechTile key={lang.name} icon={lang.icon} label={lang.name} />
|
|
460
|
-
))}
|
|
461
|
-
</div>
|
|
462
|
-
</div>
|
|
463
|
-
</ITGrid>
|
|
464
509
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
510
|
+
{/* Banda de métricas */}
|
|
511
|
+
<div className={`border-y ${FILETE}`}>
|
|
512
|
+
<dl
|
|
513
|
+
aria-label={t("showcase.home.ariaStats")}
|
|
514
|
+
className={`${HOJA} grid grid-cols-2 sm:grid-cols-4`}
|
|
515
|
+
>
|
|
516
|
+
{METRICAS.map((metrica, indice) => (
|
|
517
|
+
<div
|
|
518
|
+
key={metrica.label}
|
|
519
|
+
className={`py-5 ${indice > 0 ? "sm:border-l sm:pl-6 " : ""}${
|
|
520
|
+
indice % 2 === 1 ? "border-l pl-6 " : ""
|
|
521
|
+
}border-slate-200 dark:border-slate-800`}
|
|
522
|
+
>
|
|
523
|
+
<dt className={EYEBROW}>{metrica.label}</dt>
|
|
524
|
+
<dd className="mt-2 font-display text-[30px] font-bold leading-none tracking-[-0.02em] tabular-nums text-slate-900 dark:text-white">
|
|
525
|
+
{metrica.value}
|
|
526
|
+
</dd>
|
|
527
|
+
</div>
|
|
528
|
+
))}
|
|
529
|
+
</dl>
|
|
530
|
+
</div>
|
|
531
|
+
</section>
|
|
532
|
+
|
|
533
|
+
{/* ─── 02 · STACK (datasheet) ─── */}
|
|
534
|
+
<section id="stack" className={ANCLA}>
|
|
535
|
+
<div className={`${HOJA} py-12 sm:py-16`}>
|
|
536
|
+
<SectionHeading
|
|
537
|
+
index={1}
|
|
538
|
+
label={t("showcase.home.eyebrowStack")}
|
|
539
|
+
title={t("showcase.home.titleStack")}
|
|
540
|
+
subtitle={t("showcase.home.subtitleStack")}
|
|
541
|
+
actions={
|
|
542
|
+
<div className="relative w-full sm:w-[260px]">
|
|
543
|
+
<FaSearch
|
|
544
|
+
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
|
545
|
+
size={11}
|
|
546
|
+
/>
|
|
547
|
+
<input
|
|
548
|
+
type="search"
|
|
549
|
+
placeholder={t("showcase.home.searchTech")}
|
|
550
|
+
aria-label={t("showcase.home.searchTechAria")}
|
|
551
|
+
value={techSearch}
|
|
552
|
+
onChange={(e) => setTechSearch(e.target.value)}
|
|
553
|
+
className="w-full rounded-[10px] border border-slate-200 bg-white py-2 pl-8 pr-3 font-mono text-xs text-slate-800 outline-none transition-colors focus:border-primary-600 dark:border-slate-800 dark:bg-slate-900 dark:text-white"
|
|
554
|
+
/>
|
|
555
|
+
</div>
|
|
556
|
+
}
|
|
557
|
+
/>
|
|
558
|
+
|
|
559
|
+
{filas.length > 0 ? (
|
|
560
|
+
<dl className={`border-t ${FILETE}`}>
|
|
561
|
+
{filas.map((fila) => (
|
|
562
|
+
<div
|
|
563
|
+
key={fila.category}
|
|
564
|
+
className={`grid gap-2 border-b py-4 sm:grid-cols-[150px_minmax(0,1fr)] sm:gap-6 ${FILETE}`}
|
|
565
|
+
>
|
|
566
|
+
<dt className="font-mono text-[11px] uppercase leading-6 tracking-[0.12em] text-slate-500 dark:text-slate-400">
|
|
567
|
+
{t(fila.key)}
|
|
568
|
+
</dt>
|
|
569
|
+
<dd className="flex flex-wrap items-center gap-x-5 gap-y-2.5">
|
|
570
|
+
{fila.items.map((tech) => (
|
|
571
|
+
<span
|
|
572
|
+
key={tech.name}
|
|
573
|
+
className="inline-flex items-center gap-2 text-[14px] text-slate-700 dark:text-slate-200"
|
|
574
|
+
>
|
|
575
|
+
<TechIconImg icon={tech.icon} alt={tech.name} />
|
|
576
|
+
{tech.name}
|
|
577
|
+
</span>
|
|
578
|
+
))}
|
|
579
|
+
</dd>
|
|
580
|
+
</div>
|
|
473
581
|
))}
|
|
474
|
-
</
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
{/* ─── STACK TECNOLÓGICO ─── */}
|
|
481
|
-
<section>
|
|
482
|
-
<SectionHeading
|
|
483
|
-
eyebrow="Stack"
|
|
484
|
-
title="Tecnologías que uso"
|
|
485
|
-
subtitle="Filtra por área o busca por nombre."
|
|
486
|
-
className="!mb-5"
|
|
487
|
-
/>
|
|
488
|
-
|
|
489
|
-
<ITFlex align="center" justify="between" wrap="wrap" gap={4} className="!mb-6">
|
|
490
|
-
<ITFlex gap={2} wrap="wrap">
|
|
491
|
-
{categoriesList.map((cat) => (
|
|
492
|
-
<button
|
|
493
|
-
key={cat.id}
|
|
494
|
-
type="button"
|
|
495
|
-
onClick={() => setSelectedCategory(cat.id)}
|
|
496
|
-
className={`inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition-all ${
|
|
497
|
-
selectedCategory === cat.id
|
|
498
|
-
? "bg-primary-500 text-white shadow-sm"
|
|
499
|
-
: "bg-slate-100 text-slate-500 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
|
|
500
|
-
}`}
|
|
582
|
+
</dl>
|
|
583
|
+
) : (
|
|
584
|
+
<ITText
|
|
585
|
+
as="p"
|
|
586
|
+
className={`border-y py-10 text-center !text-sm text-slate-500 dark:text-slate-400 ${FILETE}`}
|
|
501
587
|
>
|
|
502
|
-
{
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
))}
|
|
506
|
-
</ITFlex>
|
|
507
|
-
|
|
508
|
-
<div className="relative w-full sm:w-64">
|
|
509
|
-
<FaSearch className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={12} />
|
|
510
|
-
<input
|
|
511
|
-
type="text"
|
|
512
|
-
placeholder="Buscar tecnología..."
|
|
513
|
-
value={techSearch}
|
|
514
|
-
onChange={(e) => setTechSearch(e.target.value)}
|
|
515
|
-
className="w-full rounded-xl border border-slate-200 bg-white py-2 pl-8 pr-4 text-xs text-slate-800 outline-none transition-all focus:border-primary-400 focus:ring-2 focus:ring-primary-500/30 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
|
|
516
|
-
/>
|
|
517
|
-
</div>
|
|
518
|
-
</ITFlex>
|
|
588
|
+
{t("showcase.home.noTech", { term: techSearch })}
|
|
589
|
+
</ITText>
|
|
590
|
+
)}
|
|
519
591
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
</ITGrid>
|
|
526
|
-
))}
|
|
527
|
-
</ITGrid>
|
|
528
|
-
) : (
|
|
529
|
-
<div className="rounded-2xl border border-slate-200 bg-white py-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
|
530
|
-
<ITText as="p" className="!text-sm text-slate-500 dark:text-slate-400">
|
|
531
|
-
No se encontraron tecnologías para “{techSearch}”.
|
|
592
|
+
<ITText
|
|
593
|
+
as="p"
|
|
594
|
+
className="mt-4 font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
|
|
595
|
+
>
|
|
596
|
+
{t("showcase.home.techCount", { shown: visibles, total: TECH_STACK.length })}
|
|
532
597
|
</ITText>
|
|
533
598
|
</div>
|
|
534
|
-
|
|
599
|
+
</section>
|
|
535
600
|
|
|
536
|
-
|
|
537
|
-
<
|
|
538
|
-
{
|
|
539
|
-
</ITText>
|
|
540
|
-
</section>
|
|
541
|
-
|
|
542
|
-
{/* ─── PRODUCTOS ─── */}
|
|
543
|
-
<section>
|
|
544
|
-
<SectionHeading
|
|
545
|
-
eyebrow="Portafolio"
|
|
546
|
-
title="Productos que construí"
|
|
547
|
-
subtitle="Los que tienen capturas se abren en su pantalla de detalle."
|
|
548
|
-
className="!mb-5"
|
|
549
|
-
/>
|
|
550
|
-
|
|
551
|
-
<ITGrid container spacing={5}>
|
|
552
|
-
{PRODUCTOS.map((producto) => (
|
|
553
|
-
<ITGrid item xs={12} md={6} key={producto.id}>
|
|
554
|
-
<TarjetaProducto producto={producto} onVerDetalle={setDetalle} />
|
|
555
|
-
</ITGrid>
|
|
556
|
-
))}
|
|
557
|
-
</ITGrid>
|
|
558
|
-
</section>
|
|
559
|
-
|
|
560
|
-
{/* ─── CIERRE ─── */}
|
|
561
|
-
<section className="relative overflow-hidden rounded-3xl border border-primary-200 bg-gradient-to-br from-primary-50 via-white to-purple-50 p-6 shadow-sm dark:border-primary-900/40 dark:from-primary-950/30 dark:via-slate-900 dark:to-purple-950/30 sm:p-10">
|
|
562
|
-
<div aria-hidden className="it-landing-float pointer-events-none absolute -top-20 -right-16 h-56 w-56 rounded-full bg-primary-400/20 blur-3xl" />
|
|
563
|
-
<div aria-hidden className="it-landing-grid pointer-events-none absolute inset-0 text-slate-400/40 dark:text-slate-600/30" />
|
|
564
|
-
|
|
565
|
-
<div className="relative z-10">
|
|
566
|
-
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between lg:gap-10">
|
|
601
|
+
{/* ─── 03 · PRODUCTOS ─── */}
|
|
602
|
+
<section id="proyectos" className={ANCLA}>
|
|
603
|
+
<div className={`${HOJA} border-t pb-14 pt-12 sm:pt-16 ${FILETE}`}>
|
|
567
604
|
<SectionHeading
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
605
|
+
index={2}
|
|
606
|
+
label={t("showcase.home.eyebrowPortfolio")}
|
|
607
|
+
title={t("showcase.home.titleProducts")}
|
|
608
|
+
subtitle={t("showcase.home.subtitleProducts")}
|
|
571
609
|
/>
|
|
572
610
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
611
|
+
{/* Destacado: copy a la izquierda, captura a la derecha */}
|
|
612
|
+
<article className={`${CARD} overflow-hidden`}>
|
|
613
|
+
<div className="grid lg:grid-cols-12">
|
|
614
|
+
<div className="flex flex-col p-6 sm:p-8 lg:col-span-5">
|
|
615
|
+
<ITFlex align="center" gap={2.5} wrap="wrap">
|
|
616
|
+
<ITText as="span" className={EYEBROW}>
|
|
617
|
+
<span className="tabular-nums">
|
|
618
|
+
{numero(PRODUCTOS.findIndex((p) => p.id === destacado.id))}
|
|
619
|
+
</span>
|
|
620
|
+
{` — ${t("showcase.home.featuredLabel")}`}
|
|
621
|
+
</ITText>
|
|
622
|
+
<Etiqueta>{destacado.tipo}</Etiqueta>
|
|
623
|
+
</ITFlex>
|
|
624
|
+
|
|
625
|
+
<ITText
|
|
626
|
+
as="h3"
|
|
627
|
+
className="font-display !mt-4 !text-[26px] !font-bold -tracking-[0.02em] text-slate-900 dark:text-white"
|
|
628
|
+
>
|
|
629
|
+
{destacado.nombre}
|
|
630
|
+
</ITText>
|
|
631
|
+
|
|
632
|
+
<ITText
|
|
633
|
+
as="p"
|
|
634
|
+
className="!mt-3 !text-[14px] !leading-relaxed text-slate-600 dark:text-slate-300"
|
|
635
|
+
>
|
|
636
|
+
{destacado.descripcion}
|
|
637
|
+
</ITText>
|
|
638
|
+
|
|
639
|
+
{!!destacado.caracteristicas?.length && (
|
|
640
|
+
<ul className="mt-5 space-y-2.5">
|
|
641
|
+
{destacado.caracteristicas.slice(0, 3).map((caracteristica) => (
|
|
642
|
+
<li key={caracteristica} className="flex gap-2.5">
|
|
643
|
+
<FaCheck
|
|
644
|
+
size={10}
|
|
645
|
+
className="mt-1.5 shrink-0 text-primary-700 dark:text-primary-500"
|
|
646
|
+
/>
|
|
647
|
+
<ITText
|
|
648
|
+
as="span"
|
|
649
|
+
className="!text-[13px] !leading-relaxed text-slate-600 dark:text-slate-300"
|
|
650
|
+
>
|
|
651
|
+
{caracteristica}
|
|
652
|
+
</ITText>
|
|
653
|
+
</li>
|
|
654
|
+
))}
|
|
655
|
+
</ul>
|
|
656
|
+
)}
|
|
657
|
+
|
|
658
|
+
<ITFlex gap={1.5} wrap="wrap" className="!mt-5">
|
|
659
|
+
{destacado.stack.map((tecnologia) => (
|
|
660
|
+
<span key={tecnologia} className={CHIP}>
|
|
661
|
+
{tecnologia}
|
|
662
|
+
</span>
|
|
663
|
+
))}
|
|
664
|
+
</ITFlex>
|
|
665
|
+
|
|
666
|
+
<ITFlex gap={3} wrap="wrap" align="center" className="!mt-6">
|
|
667
|
+
<EnlaceBoton href={`#proyecto/${destacado.id}`} tamano="sm">
|
|
668
|
+
{t("showcase.home.viewDetail")}
|
|
669
|
+
</EnlaceBoton>
|
|
670
|
+
{destacado.enlace && (
|
|
671
|
+
<a
|
|
672
|
+
href={destacado.enlace}
|
|
673
|
+
{...(enlaceDestacadoExterno
|
|
674
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
675
|
+
: {})}
|
|
676
|
+
className="inline-flex items-center gap-1.5 !text-xs !font-semibold !text-primary-700 dark:!text-primary-500"
|
|
677
|
+
>
|
|
678
|
+
{enlaceDestacadoExterno && <FaExternalLinkAlt size={10} />}
|
|
679
|
+
{destacado.enlaceTexto ?? t("showcase.home.viewRepo")}
|
|
680
|
+
</a>
|
|
681
|
+
)}
|
|
682
|
+
</ITFlex>
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
{portadaDestacada && (
|
|
686
|
+
<figure className="border-t border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950 lg:col-span-7 lg:border-l lg:border-t-0">
|
|
687
|
+
<div className="p-3 sm:p-5">
|
|
688
|
+
<img
|
|
689
|
+
src={portadaDestacada.src}
|
|
690
|
+
alt={`${destacado.nombre}: ${portadaDestacada.titulo}`}
|
|
691
|
+
className="w-full rounded-[6px] border border-slate-200 object-cover object-top dark:border-slate-800"
|
|
692
|
+
/>
|
|
693
|
+
</div>
|
|
694
|
+
<figcaption className="px-3 pb-4 font-mono text-[11px] leading-relaxed text-slate-500 dark:text-slate-400 sm:px-5">
|
|
695
|
+
{portadaDestacada.titulo}
|
|
696
|
+
</figcaption>
|
|
697
|
+
</figure>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
</article>
|
|
701
|
+
|
|
702
|
+
{/* Resto: rejilla de tres */}
|
|
703
|
+
<ITGrid container spacing={5} className="!mt-6">
|
|
704
|
+
{resto.map((producto) => (
|
|
705
|
+
<ITGrid item xs={12} md={6} lg={4} key={producto.id}>
|
|
706
|
+
<TarjetaProducto
|
|
707
|
+
producto={producto}
|
|
708
|
+
index={PRODUCTOS.findIndex((p) => p.id === producto.id)}
|
|
709
|
+
/>
|
|
710
|
+
</ITGrid>
|
|
711
|
+
))}
|
|
712
|
+
</ITGrid>
|
|
713
|
+
</div>
|
|
714
|
+
</section>
|
|
715
|
+
|
|
716
|
+
{/* ─── 04 · CONTACTO ─── */}
|
|
717
|
+
<section id="contacto" className={ANCLA}>
|
|
718
|
+
<div className={`${HOJA} border-t py-14 sm:py-16 ${FILETE}`}>
|
|
719
|
+
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
|
|
720
|
+
<div className="min-w-0">
|
|
721
|
+
<ITText as="p" className={EYEBROW}>
|
|
722
|
+
<span className="tabular-nums">04</span>
|
|
723
|
+
{` — ${t("showcase.home.navContact")}`}
|
|
724
|
+
</ITText>
|
|
725
|
+
<ITText
|
|
726
|
+
as="h2"
|
|
727
|
+
className="font-display !mt-3 !text-[32px] !font-bold !leading-[1.08] -tracking-[0.02em] text-slate-900 dark:text-white sm:!text-[44px]"
|
|
728
|
+
>
|
|
729
|
+
{t("showcase.home.titleTogether")}
|
|
730
|
+
</ITText>
|
|
731
|
+
<ITText
|
|
732
|
+
as="p"
|
|
733
|
+
className="!mt-4 max-w-[58ch] !text-[15px] !leading-relaxed text-slate-600 dark:text-slate-300"
|
|
734
|
+
>
|
|
735
|
+
{t("showcase.home.subtitleTogether")}
|
|
736
|
+
</ITText>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<ITFlex gap={3} wrap="wrap" align="center">
|
|
740
|
+
<EnlaceBoton href={CV_URL} descargar icono={<FaDownload size={12} />}>
|
|
741
|
+
{t("showcase.home.downloadCv")}
|
|
742
|
+
</EnlaceBoton>
|
|
743
|
+
<EnlaceBoton
|
|
744
|
+
href={LINKEDIN_URL}
|
|
745
|
+
variante="outlined"
|
|
746
|
+
icono={<FaLinkedin size={12} />}
|
|
747
|
+
>
|
|
581
748
|
LinkedIn
|
|
582
|
-
</
|
|
749
|
+
</EnlaceBoton>
|
|
750
|
+
</ITFlex>
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<ITText
|
|
754
|
+
as="p"
|
|
755
|
+
className="!mt-8 font-mono !text-[11px] !tracking-[0.06em] text-slate-500 dark:text-slate-400"
|
|
756
|
+
>
|
|
757
|
+
{t("showcase.home.writingPrefix")}{" "}
|
|
758
|
+
<a
|
|
759
|
+
href={MEDIUM_URL}
|
|
760
|
+
target="_blank"
|
|
761
|
+
rel="noopener noreferrer"
|
|
762
|
+
className="!font-semibold !text-primary-700 dark:!text-primary-500"
|
|
763
|
+
>
|
|
764
|
+
Medium
|
|
765
|
+
</a>{" "}
|
|
766
|
+
{t("showcase.home.codeOn")}{" "}
|
|
767
|
+
<a
|
|
768
|
+
href={GITHUB_URL}
|
|
769
|
+
target="_blank"
|
|
770
|
+
rel="noopener noreferrer"
|
|
771
|
+
className="!font-semibold !text-primary-700 dark:!text-primary-500"
|
|
772
|
+
>
|
|
773
|
+
GitHub
|
|
583
774
|
</a>
|
|
584
|
-
|
|
775
|
+
.
|
|
776
|
+
</ITText>
|
|
585
777
|
</div>
|
|
778
|
+
</section>
|
|
779
|
+
</main>
|
|
586
780
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
781
|
+
{/* ─── PIE ─── */}
|
|
782
|
+
<footer className={`border-t ${FILETE}`}>
|
|
783
|
+
<div
|
|
784
|
+
className={`${HOJA} flex flex-col items-start justify-between gap-3 py-7 sm:flex-row sm:items-center`}
|
|
785
|
+
>
|
|
786
|
+
<ITText
|
|
787
|
+
as="p"
|
|
788
|
+
className="font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
|
|
789
|
+
>
|
|
790
|
+
{`Asael Amaro · ${t("showcase.home.role")}`}
|
|
791
|
+
</ITText>
|
|
792
|
+
<div className="flex flex-wrap items-center gap-x-5 gap-y-2">
|
|
793
|
+
<ITText
|
|
794
|
+
as="span"
|
|
795
|
+
className="font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
|
|
594
796
|
>
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
</a>{" "}
|
|
598
|
-
y el código de mis productos está en{" "}
|
|
797
|
+
{t("showcase.home.badge")}
|
|
798
|
+
</ITText>
|
|
599
799
|
<a
|
|
600
|
-
href={
|
|
601
|
-
|
|
602
|
-
rel="noopener noreferrer"
|
|
603
|
-
className="inline-flex items-center gap-1 font-semibold text-primary-600 dark:text-primary-400"
|
|
800
|
+
href={SANDBOX_URL}
|
|
801
|
+
className="inline-flex items-center gap-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 transition-colors hover:text-primary-700 dark:text-slate-400 dark:hover:text-primary-500"
|
|
604
802
|
>
|
|
605
|
-
|
|
606
|
-
|
|
803
|
+
{t("showcase.home.footerBuiltWith")}
|
|
804
|
+
<FaArrowRight size={10} />
|
|
607
805
|
</a>
|
|
608
|
-
|
|
609
|
-
</ITText>
|
|
806
|
+
</div>
|
|
610
807
|
</div>
|
|
611
|
-
</
|
|
808
|
+
</footer>
|
|
612
809
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
</ITStack>
|
|
810
|
+
<VolverArriba />
|
|
811
|
+
</>
|
|
616
812
|
);
|
|
617
813
|
};
|